mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
fix(postgres): preserve serial aliases in agent table DDL
Closes #10678
This commit is contained in:
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user