fix(postgres): preserve serial aliases in agent table DDL

Closes #10678
This commit is contained in:
t8y2
2026-09-29 13:45:52 +00:00
parent edd2592869
commit e00af09691
2 changed files with 200 additions and 4 deletions
@@ -280,12 +280,13 @@ public abstract class PostgresLikeAgent extends AbstractJdbcAgent {
// Table comment is optional; DDL generation should still succeed without it.
}
List<ColumnInfo> 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<String> primaryKeys = primaryKeys(schema, table, attributeCache);
List<ColumnInfo> 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<ColumnInfo> postgresDdlColumns(List<ColumnInfo> columns) {
List<ColumnInfo> 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;
@@ -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<ColumnInfo> 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<ColumnInfo> 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<IndexInfo> listIndexes(String schema, String table) {
return java.util.Collections.emptyList();
}
@Override
public List<ForeignKeyInfo> 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() {