fix(kingbase): preserve SQLServer identity metadata

This commit is contained in:
t8y2 committed 2026-07-15 13:01:18 +08:00
1 parent 002dbbf602
commit 904863a57c
6 files changed
+192 -2

No files matched your search

@@ -4,8 +4,15 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class DdlBuilder { public final class DdlBuilder {
private static final Pattern SQLSERVER_IDENTITY = Pattern.compile(
"^\\s*identity\\s*\\(\\s*(-?\\d+)\\s*,\\s*(-?\\d+)\\s*\\)\\s*$",
Pattern.CASE_INSENSITIVE
);
private DdlBuilder() { private DdlBuilder() {
} }
@@ -84,6 +91,10 @@ public final class DdlBuilder {
line.append(quoteIdent(column.getName(), useBacktick)); line.append(quoteIdent(column.getName(), useBacktick));
line.append(" "); line.append(" ");
line.append(columnTypeSql(column)); line.append(columnTypeSql(column));
String identityClause = sqlServerIdentityClause(column.getExtra());
if (identityClause != null) {
line.append(" ").append(identityClause);
}
if (!column.getIs_nullable()) { if (!column.getIs_nullable()) {
line.append(" NOT NULL"); line.append(" NOT NULL");
} }
@@ -230,6 +241,14 @@ public final class DdlBuilder {
return "numeric".equals(normalized) || "decimal".equals(normalized); return "numeric".equals(normalized) || "decimal".equals(normalized);
} }
private static String sqlServerIdentityClause(String extra) {
if (extra == null) {
return null;
}
Matcher matcher = SQLSERVER_IDENTITY.matcher(extra);
return matcher.matches() ? "IDENTITY(" + matcher.group(1) + "," + matcher.group(2) + ")" : null;
}
private static boolean notBlank(String value) { private static boolean notBlank(String value) {
return value != null && !value.trim().isEmpty(); return value != null && !value.trim().isEmpty();
} }
@@ -47,6 +47,7 @@ public final class KingbaseAgent extends PostgresLikeAgent {
private static final String KINGBASE_MATVIEW_NAME = "CAST(mv.matviewname AS varchar(256))"; private static final String KINGBASE_MATVIEW_NAME = "CAST(mv.matviewname AS varchar(256))";
private static final String KINGBASE_MATVIEW_SCHEMA = "CAST(mv.schemaname AS varchar(256))"; private static final String KINGBASE_MATVIEW_SCHEMA = "CAST(mv.schemaname AS varchar(256))";
private boolean postgresCatalogMode; private boolean postgresCatalogMode;
private boolean sqlServerIdentityCatalogMode;
public static final PostgresLikeAgentProfile KINGBASE_PROFILE = new PostgresLikeAgentProfile( public static final PostgresLikeAgentProfile KINGBASE_PROFILE = new PostgresLikeAgentProfile(
"com.kingbase8.Driver", "com.kingbase8.Driver",
@@ -60,6 +61,7 @@ public final class KingbaseAgent extends PostgresLikeAgent {
@Override @Override
protected void afterConnect(ConnectParams params, Connection connection) { protected void afterConnect(ConnectParams params, Connection connection) {
postgresCatalogMode = false; postgresCatalogMode = false;
sqlServerIdentityCatalogMode = false;
setMysqlCompatMode(params.isMysql_compat_mode()); setMysqlCompatMode(params.isMysql_compat_mode());
if (params.isMysql_compat_mode()) { if (params.isMysql_compat_mode()) {
return; return;
@@ -69,6 +71,10 @@ public final class KingbaseAgent extends PostgresLikeAgent {
if (!postgresCatalogMode && mysqlSqlModeExists(connection)) { if (!postgresCatalogMode && mysqlSqlModeExists(connection)) {
setMysqlCompatMode(true); setMysqlCompatMode(true);
} }
// SQLServer compatibility exposes identity metadata through this catalog only.
sqlServerIdentityCatalogMode = !postgresCatalogMode
&& !isMysqlCompatMode()
&& catalogExists(connection, "sys.identity_columns");
} }
private static boolean mysqlSqlModeExists(Connection connection) { private static boolean mysqlSqlModeExists(Connection connection) {
@@ -413,13 +419,19 @@ public final class KingbaseAgent extends PostgresLikeAgent {
"CASE WHEN t.typname = 'numeric' AND a.atttypmod > 0 " + "CASE WHEN t.typname = 'numeric' AND a.atttypmod > 0 " +
"THEN (a.atttypmod - 4) & 65535 ELSE NULL END AS numeric_scale, " + "THEN (a.atttypmod - 4) & 65535 ELSE NULL END AS numeric_scale, " +
"CASE WHEN t.typname IN ('varchar', 'bpchar') AND a.atttypmod > 0 " + "CASE WHEN t.typname IN ('varchar', 'bpchar') AND a.atttypmod > 0 " +
"THEN a.atttypmod - 4 ELSE NULL END AS character_maximum_length " + "THEN a.atttypmod - 4 ELSE NULL END AS character_maximum_length, " +
(sqlServerIdentityCatalogMode
? "ic.seed_value AS identity_seed, ic.increment_value AS identity_increment "
: "NULL AS identity_seed, NULL AS identity_increment ") +
"FROM sys_catalog.sys_attribute a " + "FROM sys_catalog.sys_attribute a " +
"JOIN sys_catalog.sys_type t ON t.oid = a.atttypid " + "JOIN sys_catalog.sys_type t ON t.oid = a.atttypid " +
"JOIN sys_catalog.sys_class c ON c.oid = a.attrelid " + "JOIN sys_catalog.sys_class c ON c.oid = a.attrelid " +
"JOIN sys_catalog.sys_namespace n ON n.oid = c.relnamespace " + "JOIN sys_catalog.sys_namespace n ON n.oid = c.relnamespace " +
"LEFT JOIN sys_catalog.sys_attrdef ad ON ad.adrelid = a.attrelid AND ad.adnum = a.attnum " + "LEFT JOIN sys_catalog.sys_attrdef ad ON ad.adrelid = a.attrelid AND ad.adnum = a.attnum " +
"LEFT JOIN sys_catalog.sys_description d ON d.objoid = a.attrelid AND d.objsubid = a.attnum " + "LEFT JOIN sys_catalog.sys_description d ON d.objoid = a.attrelid AND d.objsubid = a.attnum " +
(sqlServerIdentityCatalogMode
? "LEFT JOIN sys.identity_columns ic ON ic.object_id = c.oid AND ic.column_id = a.attnum "
: "") +
"WHERE n.nspname = " + sqlString(effectiveSchema(schema)) + "WHERE n.nspname = " + sqlString(effectiveSchema(schema)) +
" AND c.relname = " + sqlString(table) + " " + " AND c.relname = " + sqlString(table) + " " +
"AND a.attnum > 0 AND NOT a.attisdropped " + "AND a.attnum > 0 AND NOT a.attisdropped " +
@@ -434,7 +446,7 @@ public final class KingbaseAgent extends PostgresLikeAgent {
rs.getBoolean("is_nullable"), rs.getBoolean("is_nullable"),
rs.getString("column_default"), rs.getString("column_default"),
primaryKeys.contains(columnName), primaryKeys.contains(columnName),
null, identityExtra(rs),
rs.getString("column_comment"), rs.getString("column_comment"),
intObject(rs, "numeric_precision"), intObject(rs, "numeric_precision"),
intObject(rs, "numeric_scale"), intObject(rs, "numeric_scale"),
@@ -447,6 +459,15 @@ public final class KingbaseAgent extends PostgresLikeAgent {
}); });
} }
private static String identityExtra(ResultSet rs) throws Exception {
String seed = rs.getString("identity_seed");
String increment = rs.getString("identity_increment");
if (seed == null || increment == null) {
return null;
}
return "identity(" + seed + "," + increment + ")";
}
private List<ColumnInfo> getInformationSchemaColumns(String schema, String table, Set<String> primaryKeys) { private List<ColumnInfo> getInformationSchemaColumns(String schema, String table, Set<String> primaryKeys) {
return unchecked(() -> { return unchecked(() -> {
List<ColumnInfo> result = new ArrayList<>(); List<ColumnInfo> result = new ArrayList<>();
@@ -4,6 +4,7 @@ import com.dbx.agent.ColumnInfo;
import com.dbx.agent.ConnectParams; import com.dbx.agent.ConnectParams;
import com.dbx.agent.DatabaseAgent; import com.dbx.agent.DatabaseAgent;
import com.dbx.agent.DatabaseInfo; import com.dbx.agent.DatabaseInfo;
import com.dbx.agent.DdlBuilder;
import com.dbx.agent.IndexInfo; import com.dbx.agent.IndexInfo;
import com.dbx.agent.MetadataListConstraints; import com.dbx.agent.MetadataListConstraints;
import com.dbx.agent.ObjectInfo; import com.dbx.agent.ObjectInfo;
@@ -27,6 +28,7 @@ import java.sql.Timestamp;
import java.sql.Types; import java.sql.Types;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.List; import java.util.List;
class KingbaseAgentTest extends JdbcFakeExecutionBehaviorTest { class KingbaseAgentTest extends JdbcFakeExecutionBehaviorTest {
@@ -154,6 +156,22 @@ class KingbaseAgentTest extends JdbcFakeExecutionBehaviorTest {
Assertions.assertEquals("SELECT 1 FROM sys_settings WHERE LOWER(name) = 'sql_mode'", sql.get(1)); Assertions.assertEquals("SELECT 1 FROM sys_settings WHERE LOWER(name) = 'sql_mode'", sql.get(1));
} }
@Test
void detectsSqlServerIdentityCatalogMode() throws Exception {
List<String> sql = new ArrayList<>();
KingbaseAgent agent = new KingbaseAgent();
Connection connection = sqlServerCompatConnection(sql);
Method afterConnect = KingbaseAgent.class.getDeclaredMethod("afterConnect", ConnectParams.class, Connection.class);
afterConnect.setAccessible(true);
afterConnect.invoke(agent, new ConnectParams(), connection);
Assertions.assertTrue(isSqlServerIdentityCatalogMode(agent));
Assertions.assertEquals("SELECT 1 FROM sys_catalog.sys_namespace WHERE 1 = 0", sql.get(0));
Assertions.assertEquals("SELECT 1 FROM sys_settings WHERE LOWER(name) = 'sql_mode'", sql.get(1));
Assertions.assertEquals("SELECT 1 FROM sys.identity_columns WHERE 1 = 0", sql.get(2));
}
@Test @Test
void mysqlCompatListTablesUsesInformationSchema() { void mysqlCompatListTablesUsesInformationSchema() {
List<String> sql = new ArrayList<>(); List<String> sql = new ArrayList<>();
@@ -406,6 +424,7 @@ class KingbaseAgentTest extends JdbcFakeExecutionBehaviorTest {
Assertions.assertTrue(sql.get(1).contains("format_type(a.atttypid, a.atttypmod) AS data_type"), sql.get(1)); Assertions.assertTrue(sql.get(1).contains("format_type(a.atttypid, a.atttypmod) AS data_type"), sql.get(1));
Assertions.assertTrue(sql.get(1).contains("FROM sys_catalog.sys_attribute"), sql.get(1)); Assertions.assertTrue(sql.get(1).contains("FROM sys_catalog.sys_attribute"), sql.get(1));
Assertions.assertFalse(sql.get(1).contains("information_schema.columns"), sql.get(1)); Assertions.assertFalse(sql.get(1).contains("information_schema.columns"), sql.get(1));
Assertions.assertNull(columns.get(0).getExtra());
} }
@Test @Test
@@ -439,6 +458,59 @@ class KingbaseAgentTest extends JdbcFakeExecutionBehaviorTest {
Assertions.assertEquals("identifier", columns.get(0).getComment()); Assertions.assertEquals("identifier", columns.get(0).getComment());
Assertions.assertTrue(sql.get(1).contains("FROM information_schema.columns"), sql.get(1)); Assertions.assertTrue(sql.get(1).contains("FROM information_schema.columns"), sql.get(1));
Assertions.assertTrue(sql.get(1).contains("LEFT JOIN sys_catalog.sys_description"), sql.get(1)); Assertions.assertTrue(sql.get(1).contains("LEFT JOIN sys_catalog.sys_description"), sql.get(1));
Assertions.assertNull(columns.get(0).getExtra());
}
@Test
void sqlServerCompatGetColumnsPreservesIdentityMetadata() throws Exception {
List<String> sql = new ArrayList<>();
KingbaseAgent agent = new KingbaseAgent();
setSqlServerIdentityCatalogMode(agent, true);
TestSupport.setPrivateConnection(agent, preparedConnection(sql,
resultSet(new String[]{"column_name"}, new Object[][]{{"id"}}),
resultSet(
new String[]{
"column_name",
"data_type",
"is_nullable",
"column_default",
"column_comment",
"numeric_precision",
"numeric_scale",
"character_maximum_length",
"identity_seed",
"identity_increment"
},
new Object[][]{
{"id", "int", false, null, null, 32, 0, null, "1", "1"},
{"name", "character varying", true, null, null, null, null, 64, null, null}
}
)
));
List<ColumnInfo> columns = agent.getColumns("dbo", "orders");
Assertions.assertEquals("identity(1,1)", columns.get(0).getExtra());
Assertions.assertNull(columns.get(1).getExtra());
Assertions.assertTrue(sql.get(1).contains("LEFT JOIN sys.identity_columns"), sql.get(1));
}
@Test
void ddlRendersOnlyWellFormedSqlServerIdentityMetadata() {
ColumnInfo identity = new ColumnInfo("id", "int", false, null, true, "identity(1,1)", null, null, null, null);
ColumnInfo ordinary = new ColumnInfo("name", "varchar", true, null, false, "unknown metadata", null, null, null, 64);
String ddl = DdlBuilder.buildTableDdl(
"dbo",
"orders",
Arrays.asList(identity, ordinary),
Collections.emptyList(),
Collections.emptyList()
);
Assertions.assertTrue(ddl.contains("\"id\" int IDENTITY(1,1) NOT NULL"), ddl);
Assertions.assertTrue(ddl.contains("\"name\" varchar(64)"), ddl);
Assertions.assertFalse(ddl.contains("unknown metadata"), ddl);
} }
@Test @Test
@@ -602,6 +674,23 @@ class KingbaseAgentTest extends JdbcFakeExecutionBehaviorTest {
}); });
} }
private static Connection sqlServerCompatConnection(List<String> sql) {
return proxy(Connection.class, (method, args) -> {
if ("createStatement".equals(method.getName())) {
return proxy(Statement.class, (statementMethod, statementArgs) -> {
if ("executeQuery".equals(statementMethod.getName())) {
String query = String.valueOf(statementArgs[0]);
sql.add(query);
return resultSet(new String[]{"probe"}, new Object[][]{});
}
return defaultValue(statementMethod.getReturnType());
});
}
if ("isClosed".equals(method.getName())) return false;
return defaultValue(method.getReturnType());
});
}
private static ResultSet resultSet(String[] columns, Object[][] rows) { private static ResultSet resultSet(String[] columns, Object[][] rows) {
int[] index = {-1}; int[] index = {-1};
return proxy(ResultSet.class, (method, args) -> { return proxy(ResultSet.class, (method, args) -> {
@@ -694,4 +783,16 @@ class KingbaseAgentTest extends JdbcFakeExecutionBehaviorTest {
method.setAccessible(true); method.setAccessible(true);
return method.invoke(agent, rs, 1, sqlType, columnTypeName); return method.invoke(agent, rs, 1, sqlType, columnTypeName);
} }
private static void setSqlServerIdentityCatalogMode(KingbaseAgent agent, boolean enabled) throws Exception {
java.lang.reflect.Field field = KingbaseAgent.class.getDeclaredField("sqlServerIdentityCatalogMode");
field.setAccessible(true);
field.setBoolean(agent, enabled);
}
private static boolean isSqlServerIdentityCatalogMode(KingbaseAgent agent) throws Exception {
java.lang.reflect.Field field = KingbaseAgent.class.getDeclaredField("sqlServerIdentityCatalogMode");
field.setAccessible(true);
return field.getBoolean(agent);
}
} }
@@ -16,12 +16,24 @@ import {
isMysqlEnumDataType, isMysqlEnumDataType,
isSqlServerIdentityCompatibleDataType, isSqlServerIdentityCompatibleDataType,
mysqlEnumDataType, mysqlEnumDataType,
parseExtraToColumnExtra,
rehydrateColumnDraftsFromMetadata, rehydrateColumnDraftsFromMetadata,
restoreDamengLengthUnitsAfterSave, restoreDamengLengthUnitsAfterSave,
splitDataType, splitDataType,
} from "@/lib/table/tableStructureEditorState"; } from "@/lib/table/tableStructureEditorState";
describe("tableStructureEditorState", () => { describe("tableStructureEditorState", () => {
it("parses Kingbase SQLServer compatibility identity metadata", () => {
expect(parseExtraToColumnExtra("identity(10, 2)", "kingbase")).toEqual({
autoIncrement: true,
identity: { seed: 10, increment: 2 },
});
expect(parseExtraToColumnExtra("generated always as identity", "kingbase")).toEqual({
identity: { generation: "ALWAYS" },
});
expect(parseExtraToColumnExtra("identity(1,1)", "postgres")).toEqual({});
});
it("keeps mysql unsigned attributes in the editable base type", () => { it("keeps mysql unsigned attributes in the editable base type", () => {
expect(splitDataType("int(11) unsigned")).toEqual({ baseType: "int unsigned", params: "11" }); expect(splitDataType("int(11) unsigned")).toEqual({ baseType: "int unsigned", params: "11" });
expect(splitDataType("bigint(20) unsigned zerofill")).toEqual({ expect(splitDataType("bigint(20) unsigned zerofill")).toEqual({
@@ -477,6 +477,16 @@ export function parseExtraToColumnExtra(extra: string | null | undefined, databa
result.identity.seed = Number(sequenceMatch[1]); result.identity.seed = Number(sequenceMatch[1]);
result.identity.increment = Number(sequenceMatch[2]); result.identity.increment = Number(sequenceMatch[2]);
} }
} else if (databaseType === "kingbase") {
// SQLServer compatibility reports IDENTITY(seed, increment) instead of PostgreSQL identity syntax.
const sqlServerIdentityMatch = lower.match(/identity\s*\(\s*(-?\d+)\s*,\s*(-?\d+)\s*\)/i);
if (sqlServerIdentityMatch) {
result.autoIncrement = true;
result.identity = {
seed: Number(sqlServerIdentityMatch[1]),
increment: Number(sqlServerIdentityMatch[2]),
};
}
} }
} else if (databaseType === "sqlserver") { } else if (databaseType === "sqlserver") {
if (lower.includes("identity")) { if (lower.includes("identity")) {
+27
View File
@@ -4668,6 +4668,33 @@ mod tests {
assert_eq!(result.statements, vec!["INSERT INTO `app`.`users` (`name`) VALUES ('Ada');"]); assert_eq!(result.statements, vec!["INSERT INTO `app`.`users` (`name`) VALUES ('Ada');"]);
} }
#[test]
fn prepare_data_grid_save_omits_empty_kingbase_sqlserver_identity_value() {
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
database_type: Some(DatabaseType::Kingbase),
table_meta: DataGridTableMeta {
catalog: None,
database: None,
schema: Some("dbo".to_string()),
table_name: "orders".to_string(),
primary_keys: vec!["id".to_string()],
columns: Some(vec![
pk_column("id", "int", false, Some("identity(1,1)")),
column("name", "varchar", false, None),
]),
},
columns: vec!["id".to_string(), "name".to_string()],
source_columns: None,
rows: vec![],
dirty_rows: vec![],
deleted_rows: vec![],
new_rows: vec![vec![Value::Null, json!("Ada")]],
});
assert_eq!(result.validation_error, None);
assert_eq!(result.statements, vec![r#"INSERT INTO "dbo"."orders" ("name") VALUES ('Ada');"#]);
}
#[test] #[test]
fn prepare_data_grid_save_still_validates_other_not_null_columns_in_sqlite() { fn prepare_data_grid_save_still_validates_other_not_null_columns_in_sqlite() {
let result = prepare_data_grid_save(DataGridSaveStatementOptions { let result = prepare_data_grid_save(DataGridSaveStatementOptions {