|
| 1 | +"""Integration test: PostgreSQL real-type → Java type mapping end-to-end. |
| 2 | +
|
| 3 | +Spins up PostgreSQL 16 via Testcontainers, creates a table with all the |
| 4 | +PG-specific types our dialect adapter cares about (SERIAL / BIGSERIAL / |
| 5 | +JSONB / UUID / BYTEA / TIMESTAMPTZ / NUMERIC ...), reads back what PG |
| 6 | +reports through information_schema.columns, then verifies that |
| 7 | +PostgreSQLDialect.java_type_for() produces the expected Java type for each. |
| 8 | +
|
| 9 | +Why this matters: |
| 10 | + - PG reports types in lowercase, sometimes with full names ("integer", |
| 11 | + "bigint", "timestamp with time zone", "character varying"). We need to |
| 12 | + confirm our dialect handles those exact strings, not just the abbreviated |
| 13 | + forms we hard-coded. |
| 14 | + - Catches regressions: if PG adds a new pg_type or renames one, this test |
| 15 | + fails before users hit it. |
| 16 | +
|
| 17 | +Skipped when Docker / testcontainers not available (see conftest). |
| 18 | +""" |
| 19 | + |
| 20 | +from __future__ import annotations |
| 21 | + |
| 22 | +import pytest |
| 23 | + |
| 24 | +from dbjavagenix.database.dialect import get_dialect |
| 25 | + |
| 26 | + |
| 27 | +pytest.importorskip("psycopg2", reason="psycopg2 required to talk to PG") |
| 28 | + |
| 29 | + |
| 30 | +CREATE_TABLE_SQL = """ |
| 31 | +CREATE TABLE IF NOT EXISTS dialect_check ( |
| 32 | + id BIGSERIAL PRIMARY KEY, |
| 33 | + small_int_col SMALLINT, |
| 34 | + int_col INTEGER, |
| 35 | + big_int_col BIGINT, |
| 36 | + serial_col SERIAL, |
| 37 | + big_serial_col BIGSERIAL, |
| 38 | + real_col REAL, |
| 39 | + double_col DOUBLE PRECISION, |
| 40 | + numeric_col NUMERIC(10, 2), |
| 41 | + money_col MONEY, |
| 42 | + varchar_col VARCHAR(64), |
| 43 | + text_col TEXT, |
| 44 | + char_col CHAR(8), |
| 45 | + date_col DATE, |
| 46 | + time_col TIME, |
| 47 | + timestamp_col TIMESTAMP, |
| 48 | + timestamptz_col TIMESTAMPTZ, |
| 49 | + bool_col BOOLEAN, |
| 50 | + bytea_col BYTEA, |
| 51 | + json_col JSON, |
| 52 | + jsonb_col JSONB, |
| 53 | + uuid_col UUID, |
| 54 | + inet_col INET |
| 55 | +); |
| 56 | +""" |
| 57 | + |
| 58 | + |
| 59 | +# 期望表: PG information_schema.columns 上报的 data_type 字符串 → 期望的 Java 类型 |
| 60 | +# 这些字符串来自 PG 16,实测值;若 PG 升级有变化此处会失败,需要更新 dialect.py |
| 61 | +EXPECTED_MAPPINGS = { |
| 62 | + # column_name: (expected_pg_data_type_uppercase, expected_java_type) |
| 63 | + "id": ("BIGINT", "Long"), |
| 64 | + "small_int_col": ("SMALLINT", "Short"), |
| 65 | + "int_col": ("INTEGER", "Integer"), |
| 66 | + "big_int_col": ("BIGINT", "Long"), |
| 67 | + "serial_col": ("INTEGER", "Integer"), |
| 68 | + "big_serial_col": ("BIGINT", "Long"), |
| 69 | + "real_col": ("REAL", "Float"), |
| 70 | + "double_col": ("DOUBLE PRECISION", "Double"), |
| 71 | + "numeric_col": ("NUMERIC", "BigDecimal"), |
| 72 | + "money_col": ("MONEY", "BigDecimal"), |
| 73 | + "varchar_col": ("CHARACTER VARYING", "String"), |
| 74 | + "text_col": ("TEXT", "String"), |
| 75 | + "char_col": ("CHARACTER", "String"), |
| 76 | + "date_col": ("DATE", "LocalDate"), |
| 77 | + "time_col": ("TIME", "LocalTime"), |
| 78 | + "timestamp_col": ("TIMESTAMP", "LocalDateTime"), |
| 79 | + "timestamptz_col": ("TIMESTAMP WITH TIME ZONE", "OffsetDateTime"), |
| 80 | + "bool_col": ("BOOLEAN", "Boolean"), |
| 81 | + "bytea_col": ("BYTEA", "byte[]"), |
| 82 | + "json_col": ("JSON", "String"), |
| 83 | + "jsonb_col": ("JSONB", "String"), |
| 84 | + "uuid_col": ("UUID", "String"), |
| 85 | + "inet_col": ("INET", "String"), |
| 86 | +} |
| 87 | + |
| 88 | + |
| 89 | +@pytest.fixture(scope="module") |
| 90 | +def pg_connection(postgres_container): |
| 91 | + """psycopg2 connection to the container, with the test table created.""" |
| 92 | + import psycopg2 |
| 93 | + |
| 94 | + conn = psycopg2.connect( |
| 95 | + host=postgres_container["host"], |
| 96 | + port=postgres_container["port"], |
| 97 | + user=postgres_container["username"], |
| 98 | + password=postgres_container["password"], |
| 99 | + dbname=postgres_container["database"], |
| 100 | + ) |
| 101 | + conn.autocommit = True |
| 102 | + with conn.cursor() as cur: |
| 103 | + cur.execute(CREATE_TABLE_SQL) |
| 104 | + yield conn |
| 105 | + conn.close() |
| 106 | + |
| 107 | + |
| 108 | +def _read_column_types(conn) -> dict[str, str]: |
| 109 | + """Return {column_name: data_type_uppercase} from information_schema.""" |
| 110 | + with conn.cursor() as cur: |
| 111 | + cur.execute( |
| 112 | + """ |
| 113 | + SELECT column_name, data_type |
| 114 | + FROM information_schema.columns |
| 115 | + WHERE table_name = 'dialect_check' |
| 116 | + """ |
| 117 | + ) |
| 118 | + return {row[0]: row[1].upper() for row in cur.fetchall()} |
| 119 | + |
| 120 | + |
| 121 | +def test_pg_reports_expected_data_types(pg_connection): |
| 122 | + """Sanity: PG 16 information_schema reports the data types we expect.""" |
| 123 | + actual = _read_column_types(pg_connection) |
| 124 | + for col, (expected_pg_type, _) in EXPECTED_MAPPINGS.items(): |
| 125 | + assert col in actual, f"column {col!r} missing from information_schema" |
| 126 | + assert actual[col] == expected_pg_type, ( |
| 127 | + f"column {col!r}: PG reports {actual[col]!r}, " |
| 128 | + f"expected {expected_pg_type!r}" |
| 129 | + ) |
| 130 | + |
| 131 | + |
| 132 | +def test_dialect_maps_pg_types_to_correct_java_types(pg_connection): |
| 133 | + """End-to-end: PG-reported type → PostgreSQLDialect → expected Java type.""" |
| 134 | + actual = _read_column_types(pg_connection) |
| 135 | + dialect = get_dialect("postgresql") |
| 136 | + |
| 137 | + mismatches = [] |
| 138 | + for col, (_, expected_java) in EXPECTED_MAPPINGS.items(): |
| 139 | + pg_type = actual[col] |
| 140 | + java_type = dialect.java_type_for(pg_type) |
| 141 | + if java_type != expected_java: |
| 142 | + mismatches.append( |
| 143 | + f" {col} ({pg_type}): got {java_type!r}, expected {expected_java!r}" |
| 144 | + ) |
| 145 | + |
| 146 | + assert not mismatches, "type mapping mismatches:\n" + "\n".join(mismatches) |
| 147 | + |
| 148 | + |
| 149 | +def test_serial_columns_have_default_nextval(pg_connection): |
| 150 | + """Verify SERIAL/BIGSERIAL columns get the standard sequence default — |
| 151 | + relevant for future codegen support of auto-increment annotations. |
| 152 | + """ |
| 153 | + with pg_connection.cursor() as cur: |
| 154 | + cur.execute( |
| 155 | + """ |
| 156 | + SELECT column_name, column_default |
| 157 | + FROM information_schema.columns |
| 158 | + WHERE table_name = 'dialect_check' |
| 159 | + AND column_name IN ('serial_col', 'big_serial_col', 'id') |
| 160 | + """ |
| 161 | + ) |
| 162 | + rows = dict(cur.fetchall()) |
| 163 | + |
| 164 | + for col in ("serial_col", "big_serial_col", "id"): |
| 165 | + default = rows.get(col, "") |
| 166 | + assert default and "nextval" in default, ( |
| 167 | + f"{col} expected to have nextval default, got {default!r}" |
| 168 | + ) |
0 commit comments