diff --git a/agents/common/src/main/java/com/dbx/agent/PostgresLikeAgent.java b/agents/common/src/main/java/com/dbx/agent/PostgresLikeAgent.java index ba8922e862..50f5360bb1 100644 --- a/agents/common/src/main/java/com/dbx/agent/PostgresLikeAgent.java +++ b/agents/common/src/main/java/com/dbx/agent/PostgresLikeAgent.java @@ -280,12 +280,13 @@ public abstract class PostgresLikeAgent extends AbstractJdbcAgent { // Table comment is optional; DDL generation should still succeed without it. } + List columns = attributeCache == null + ? getColumns(schema, table) + : getColumns(schema, table, attributeCache); return DdlBuilder.buildTableDdl( schema, table, - attributeCache == null - ? getColumns(schema, table) - : getColumns(schema, table, attributeCache), + postgresDdlColumns(columns), indexes, foreignKeys, checkConstraints, @@ -405,11 +406,21 @@ public abstract class PostgresLikeAgent extends AbstractJdbcAgent { return unchecked(() -> { Set primaryKeys = primaryKeys(schema, table, attributeCache); List result = new ArrayList<>(); + // Legacy serial columns own their sequence with an AUTO ('a') dependency; + // identity columns use an INTERNAL ('i') dependency and stay unmarked. String sql = "SELECT a.attname AS column_name, " + profile.catalogBuiltinFunction("format_type") + "(a.atttypid, a.atttypmod) AS data_type, " + "NOT a.attnotnull AS is_nullable, " + profile.catalogPrefixedFunction("get_expr") + "(ad.adbin, ad.adrelid) AS column_default, " + profile.catalogBuiltinFunction("col_description") + "(a.attrelid, a.attnum) AS column_comment, " + + "CASE WHEN a.atttypid IN (20, 21, 23) AND serial_seq.oid IS NOT NULL " + + "AND " + profile.catalogPrefixedFunction("get_expr") + "(ad.adbin, ad.adrelid) = " + + "format('nextval(%L::regclass)', serial_seq.oid::regclass::text) " + + "THEN CASE a.atttypid " + + "WHEN 21 THEN 'smallserial' " + + "WHEN 23 THEN 'serial' " + + "WHEN 20 THEN 'bigserial' " + + "END ELSE NULL END AS column_extra, " + "CASE WHEN t.typname = 'numeric' AND a.atttypmod > 0 " + "THEN ((a.atttypmod - 4) >> 16) & 65535 ELSE NULL END AS numeric_precision, " + "CASE WHEN t.typname = 'numeric' AND a.atttypmod > 0 " + @@ -421,6 +432,24 @@ public abstract class PostgresLikeAgent extends AbstractJdbcAgent { "JOIN " + profile.catalogRelation("class") + " c ON c.oid = a.attrelid " + "JOIN " + profile.catalogRelation("namespace") + " n ON n.oid = c.relnamespace " + "LEFT JOIN " + profile.catalogRelation("attrdef") + " ad ON ad.adrelid = a.attrelid AND ad.adnum = a.attnum " + + "LEFT JOIN " + profile.catalogRelation("class") + " serial_seq ON serial_seq.oid = (" + + "SELECT sequence_dep.objid " + + "FROM " + profile.catalogRelation("depend") + " sequence_dep " + + "JOIN " + profile.catalogRelation("class") + " sequence_class " + + "ON sequence_class.oid = sequence_dep.objid AND sequence_class.relkind = 'S' " + + "WHERE sequence_dep.classid = " + catalogRegclass("class") + " " + + "AND sequence_dep.objsubid = 0 " + + "AND sequence_dep.refclassid = " + catalogRegclass("class") + " " + + "AND sequence_dep.refobjid = a.attrelid AND sequence_dep.refobjsubid = a.attnum " + + "AND sequence_dep.deptype = 'a' AND EXISTS (" + + "SELECT 1 FROM " + profile.catalogRelation("depend") + " serial_default_dep " + + "WHERE serial_default_dep.classid = " + catalogRegclass("attrdef") + " " + + "AND serial_default_dep.objid = ad.oid AND serial_default_dep.objsubid = 0 " + + "AND serial_default_dep.refclassid = " + catalogRegclass("class") + " " + + "AND serial_default_dep.refobjid = sequence_dep.objid " + + "AND serial_default_dep.refobjsubid = 0 AND serial_default_dep.deptype = 'n'" + + ") ORDER BY sequence_dep.objid LIMIT 1) " + + "AND serial_seq.relkind = 'S' " + "WHERE n.nspname = ? AND c.relname = ? " + "AND a.attnum > 0 AND NOT a.attisdropped " + "ORDER BY a.attnum"; @@ -436,7 +465,7 @@ public abstract class PostgresLikeAgent extends AbstractJdbcAgent { rs.getBoolean("is_nullable"), rs.getString("column_default"), primaryKeys.contains(colName), - null, + rs.getString("column_extra"), rs.getString("column_comment"), intObject(rs, "numeric_precision"), intObject(rs, "numeric_scale"), @@ -1107,6 +1136,52 @@ public abstract class PostgresLikeAgent extends AbstractJdbcAgent { return quoteIdentifier(schema) + "." + quoteIdentifier(name); } + private String catalogRegclass(String relation) { + return "'" + profile.catalogRelation(relation) + "'::regclass"; + } + + /** + * PostgreSQL reports legacy serial columns as their integer type plus a + * sequence default. Convert only the positively identified markers for + * table DDL, keeping the generic DDL builder unchanged for other agents. + */ + private static List postgresDdlColumns(List columns) { + List result = new ArrayList<>(columns.size()); + for (ColumnInfo column : columns) { + String serialType = serialType(column.getExtra()); + if (serialType == null) { + result.add(column); + continue; + } + result.add(new ColumnInfo( + column.getName(), + serialType, + column.getIs_nullable(), + null, + column.getIs_primary_key(), + null, + column.getComment(), + column.getNumeric_precision(), + column.getNumeric_scale(), + column.getCharacter_maximum_length(), + column.getCharacter_set(), + column.getCollation() + )); + } + return result; + } + + private static String serialType(String extra) { + if (extra == null) { + return null; + } + String normalized = extra.trim().toLowerCase(Locale.ROOT); + if ("smallserial".equals(normalized) || "serial".equals(normalized) || "bigserial".equals(normalized)) { + return normalized; + } + return null; + } + private static Integer intObject(ResultSet rs, String column) throws Exception { Object value = rs.getObject(column); return value instanceof Number ? ((Number) value).intValue() : null; diff --git a/agents/common/src/test/java/com/dbx/agent/PostgresLikeAgentTest.java b/agents/common/src/test/java/com/dbx/agent/PostgresLikeAgentTest.java index aa1ac77816..2457226d62 100644 --- a/agents/common/src/test/java/com/dbx/agent/PostgresLikeAgentTest.java +++ b/agents/common/src/test/java/com/dbx/agent/PostgresLikeAgentTest.java @@ -15,6 +15,7 @@ import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; class PostgresLikeAgentTest { @@ -56,6 +57,80 @@ class PostgresLikeAgentTest { assertFalse(agent.getProfile().mapsCatalogAttributeArraysInJava()); } + @Test + void serialMetadataQueryRequiresAnOwnedSequenceAndExactNextvalDefault() { + TestPostgresLikeAgent agent = new TestPostgresLikeAgent(); + agent.connect(new ConnectParams()); + + agent.getColumns("app", "orders"); + + String sql = String.join("\n", MetadataSqlFake.statements); + assertTrue(sql.contains("CASE WHEN a.atttypid IN (20, 21, 23)"), sql); + assertTrue(sql.contains("sequence_dep.deptype = 'a'"), sql); + assertTrue(sql.contains("serial_default_dep.objid = ad.oid"), sql); + assertTrue(sql.contains("serial_default_dep.refobjid = sequence_dep.objid"), sql); + assertTrue(sql.contains("serial_seq.relkind = 'S'"), sql); + assertTrue(sql.contains("format('nextval(%L::regclass)', serial_seq.oid::regclass::text)"), sql); + assertTrue(sql.contains("WHEN 21 THEN 'smallserial'"), sql); + assertTrue(sql.contains("WHEN 23 THEN 'serial'"), sql); + assertTrue(sql.contains("WHEN 20 THEN 'bigserial'"), sql); + } + + @Test + void readsSerialMarkersWithoutReclassifyingPlainOrIdentityLikeColumns() { + TestPostgresLikeAgent agent = new TestPostgresLikeAgent(preparedConnection( + resultSet(new String[]{"column_name"}, new Object[][]{}), + resultSet( + new String[]{ + "column_name", "data_type", "is_nullable", "column_default", "column_extra", + "column_comment", "numeric_precision", "numeric_scale", "character_maximum_length" + }, + new Object[][]{ + {"small_id", "smallint", false, "nextval('small_id_seq'::regclass)", "smallserial", null, null, null, null}, + {"id", "bigint", false, "nextval('id_seq'::regclass)", "bigserial", null, null, null, null}, + {"plain_id", "bigint", true, null, null, null, null, null, null}, + {"manual_id", "bigint", true, "nextval('shared_id_seq'::regclass)", null, null, null, null, null}, + {"identity_id", "bigint", false, null, null, null, null, null, null} + } + ) + )); + agent.connect(new ConnectParams()); + + List columns = agent.getColumns("public", "orders"); + + assertEquals("smallserial", columns.get(0).getExtra()); + assertEquals("bigserial", columns.get(1).getExtra()); + assertNull(columns.get(2).getExtra()); + assertNull(columns.get(3).getExtra()); + assertNull(columns.get(4).getExtra()); + assertEquals("nextval('shared_id_seq'::regclass)", columns.get(3).getColumn_default()); + } + + @Test + void genericDdlBuilderDoesNotInterpretPostgresSerialMarkers() { + String ddl = DdlBuilder.buildTableDdl( + "public", + "orders", + java.util.Collections.singletonList(new ColumnInfo( + "id", + "bigint", + false, + "nextval('shared_id_seq'::regclass)", + true, + "bigserial", + null, + null, + null, + null + )), + java.util.Collections.emptyList(), + java.util.Collections.emptyList() + ); + + assertTrue(ddl.contains("\"id\" bigint NOT NULL DEFAULT nextval('shared_id_seq'::regclass)"), ddl); + assertFalse(ddl.contains("bigserial"), ddl); + } + @Test void metadataQueriesSupportRenamedPostgresCatalogs() { TestPostgresLikeAgent agent = new TestPostgresLikeAgent(new PostgresLikeAgentProfile( @@ -231,6 +306,22 @@ class PostgresLikeAgentTest { assertFalse(ddl.contains("CHECK"), ddl); } + @Test + void tableDdlRendersLegacySerialTypesAndPreservesOtherIntegerDefaults() { + SerialDdlAgent agent = new SerialDdlAgent(); + + String ddl = agent.getTableDdl("public", "orders"); + + assertTrue(ddl.startsWith("CREATE TABLE \"public\".\"orders\" (\n"), ddl); + assertTrue(ddl.contains(" \"id\" bigserial NOT NULL"), ddl); + assertTrue(ddl.contains(" \"small_id\" smallserial"), ddl); + assertTrue(ddl.contains(" \"plain_id\" bigint"), ddl); + assertTrue(ddl.contains(" \"manual_id\" bigint DEFAULT nextval('shared_id_seq'::regclass)"), ddl); + assertTrue(ddl.contains(" \"identity_id\" bigint NOT NULL DEFAULT nextval('identity_id_seq'::regclass)"), ddl); + assertFalse(ddl.contains("\"id\" bigint DEFAULT nextval"), ddl); + assertFalse(ddl.contains("\"small_id\" smallint DEFAULT nextval"), ddl); + } + @Test void postgisGeometryTypeNameDetection() { assertTrue(PostgresLikeAgent.isPostgisGeometryTypeName("geometry")); @@ -416,6 +507,36 @@ class PostgresLikeAgentTest { } } + private static final class SerialDdlAgent extends PostgresLikeAgent { + private SerialDdlAgent() { + super(new PostgresLikeAgentProfile( + PostgresLikeAgentTest.class.getName(), + "jdbc:test://{host}:{port}/{database}" + )); + } + + @Override + public List getColumns(String schema, String table) { + return java.util.Arrays.asList( + new ColumnInfo("id", "bigint", false, "nextval('id_seq'::regclass)", true, "bigserial", null, null, null, null), + new ColumnInfo("small_id", "smallint", true, "nextval('small_id_seq'::regclass)", false, "smallserial", null, null, null, null), + new ColumnInfo("plain_id", "bigint", true, null, false), + new ColumnInfo("manual_id", "bigint", true, "nextval('shared_id_seq'::regclass)", false), + new ColumnInfo("identity_id", "bigint", false, "nextval('identity_id_seq'::regclass)", false, "generated by default as identity", null, null, null, null) + ); + } + + @Override + public List listIndexes(String schema, String table) { + return java.util.Collections.emptyList(); + } + + @Override + public List listForeignKeys(String schema, String table) { + return java.util.Collections.emptyList(); + } + } + private static Connection preparedConnection(ResultSet... resultSets) { int[] resultSetIndex = {0}; return proxy(Connection.class, new MethodHandler() {