fix(grid): default keyless edits for SQL tables

This commit is contained in:
t8y2
2026-05-21 01:34:45 +08:00
parent 803f70dce9
commit f1a5beede5
4 changed files with 112 additions and 74 deletions
@@ -30,41 +30,57 @@ const DEFAULT_TABLE_DATA_CAPABILITY: TableDataCapability = {
transaction: true,
};
const NAVICAT_STYLE_TABLE_DATA_CAPABILITY: TableDataCapability = {
insert: true,
updateRequiresPrimaryKey: false,
deleteRequiresPrimaryKey: false,
keylessRowPredicate: true,
requiresTransactionalTableForExistingRows: false,
transaction: true,
};
const DEFAULT_CAPABILITY: DatabaseCapability = {
schemaAware: false,
treeSchemaMode: false,
tableData: DEFAULT_TABLE_DATA_CAPABILITY,
};
const NAVICAT_STYLE_TABLE_DATA_TYPES = new Set<DatabaseType>([
"mysql",
"postgres",
"sqlite",
"duckdb",
"sqlserver",
"oracle",
"doris",
"starrocks",
"redshift",
"dameng",
"gaussdb",
"kingbase",
"highgo",
"vastbase",
"goldendb",
"yashandb",
"databricks",
"saphana",
"teradata",
"vertica",
"firebird",
"exasol",
"opengauss",
"oceanbase-oracle",
"gbase",
"access",
"h2",
"snowflake",
"db2",
"informix",
"bigquery",
"sundb",
]);
const DATABASE_CAPABILITY_OVERRIDES: Partial<Record<DatabaseType, Partial<DatabaseCapability>>> = {
sqlite: {
tableData: {
insert: true,
updateRequiresPrimaryKey: true,
deleteRequiresPrimaryKey: true,
requiresTransactionalTableForExistingRows: false,
transaction: true,
},
},
duckdb: {
tableData: {
insert: true,
updateRequiresPrimaryKey: true,
deleteRequiresPrimaryKey: true,
requiresTransactionalTableForExistingRows: false,
transaction: true,
},
},
access: {
tableData: {
insert: true,
updateRequiresPrimaryKey: false,
deleteRequiresPrimaryKey: false,
keylessRowPredicate: true,
requiresTransactionalTableForExistingRows: false,
transaction: true,
},
},
hive: {
tableData: {
insert: true,
@@ -75,25 +91,6 @@ const DATABASE_CAPABILITY_OVERRIDES: Partial<Record<DatabaseType, Partial<Databa
transaction: false,
},
},
dameng: {
tableData: {
insert: true,
updateRequiresPrimaryKey: false,
deleteRequiresPrimaryKey: false,
keylessRowPredicate: true,
requiresTransactionalTableForExistingRows: false,
transaction: true,
},
},
informix: {
tableData: {
insert: true,
updateRequiresPrimaryKey: true,
deleteRequiresPrimaryKey: true,
requiresTransactionalTableForExistingRows: false,
transaction: true,
},
},
jdbc: {
tableData: {
insert: false,
@@ -103,15 +100,6 @@ const DATABASE_CAPABILITY_OVERRIDES: Partial<Record<DatabaseType, Partial<Databa
transaction: false,
},
},
yashandb: {
tableData: {
insert: false,
updateRequiresPrimaryKey: true,
deleteRequiresPrimaryKey: true,
requiresTransactionalTableForExistingRows: false,
transaction: false,
},
},
neo4j: {
syntheticKey: "neo4j-element-id",
},
@@ -148,15 +136,21 @@ const DATABASE_CAPABILITY_OVERRIDES: Partial<Record<DatabaseType, Partial<Databa
},
};
function defaultTableDataCapability(dbType?: DatabaseType): TableDataCapability {
if (dbType && NAVICAT_STYLE_TABLE_DATA_TYPES.has(dbType)) return NAVICAT_STYLE_TABLE_DATA_CAPABILITY;
return DEFAULT_TABLE_DATA_CAPABILITY;
}
export function getDatabaseCapability(dbType?: DatabaseType): DatabaseCapability {
const override = dbType ? DATABASE_CAPABILITY_OVERRIDES[dbType] : undefined;
const tableData = defaultTableDataCapability(dbType);
return {
...DEFAULT_CAPABILITY,
...override,
schemaAware: isSchemaAware(dbType),
treeSchemaMode: usesTreeSchemaMode(dbType),
tableData: {
...DEFAULT_TABLE_DATA_CAPABILITY,
...tableData,
...override?.tableData,
},
};
+22
View File
@@ -205,6 +205,28 @@ test("builds Hive grid save statements without primary keys using row predicates
]);
});
test("builds PostgreSQL grid save statements without primary keys using row predicates", () => {
const statements = buildDataGridSaveStatements({
databaseType: "postgres",
tableMeta: {
schema: "public",
tableName: "visits",
primaryKeys: [],
},
columns: ["id", "title", "published_at"],
rows: [[63, "old title", null]],
dirtyRows: [[0, [[1, "new title"]]]],
deletedRows: [0],
newRows: [[64, "fresh", "2026-05-20 12:00:00"]],
});
assert.deepEqual(statements, [
`UPDATE "public"."visits" SET "title" = 'new title' WHERE "id" = 63 AND "title" = 'old title' AND "published_at" IS NULL;`,
`DELETE FROM "public"."visits" WHERE "id" = 63 AND "title" = 'old title' AND "published_at" IS NULL;`,
`INSERT INTO "public"."visits" ("id", "title", "published_at") VALUES (64, 'fresh', '2026-05-20 12:00:00');`,
]);
});
test("builds Dameng grid save statements without primary keys using row predicates", () => {
const statements = buildDataGridSaveStatements({
databaseType: "dameng",
+29 -10
View File
@@ -108,27 +108,46 @@ test("describes table editing capabilities for special database engines", () =>
});
assert.deepEqual(getDatabaseCapability("yashandb").tableData, {
insert: false,
updateRequiresPrimaryKey: true,
deleteRequiresPrimaryKey: true,
keylessRowPredicate: false,
insert: true,
updateRequiresPrimaryKey: false,
deleteRequiresPrimaryKey: false,
keylessRowPredicate: true,
requiresTransactionalTableForExistingRows: false,
transaction: false,
transaction: true,
});
assert.equal(getDatabaseCapability("oracle").syntheticKey, "oracle-rowid");
assert.equal(getDatabaseCapability("neo4j").syntheticKey, "neo4j-element-id");
});
test("uses conservative table editing defaults for unknown or keyless relational engines", () => {
test("uses Navicat-style table editing defaults for updateable SQL table engines", () => {
assert.deepEqual(getDatabaseCapability("postgres").tableData, {
insert: false,
updateRequiresPrimaryKey: true,
deleteRequiresPrimaryKey: true,
keylessRowPredicate: false,
insert: true,
updateRequiresPrimaryKey: false,
deleteRequiresPrimaryKey: false,
keylessRowPredicate: true,
requiresTransactionalTableForExistingRows: false,
transaction: true,
});
assert.deepEqual(getDatabaseCapability("mysql").tableData, {
insert: true,
updateRequiresPrimaryKey: false,
deleteRequiresPrimaryKey: false,
keylessRowPredicate: true,
requiresTransactionalTableForExistingRows: false,
transaction: true,
});
assert.deepEqual(getDatabaseCapability("sqlite").tableData, {
insert: true,
updateRequiresPrimaryKey: false,
deleteRequiresPrimaryKey: false,
keylessRowPredicate: true,
requiresTransactionalTableForExistingRows: false,
transaction: true,
});
});
test("keeps conservative table editing defaults for unknown database types", () => {
assert.deepEqual(getDatabaseCapability(undefined).tableData, {
insert: false,
updateRequiresPrimaryKey: true,
+10 -7
View File
@@ -45,7 +45,7 @@ test("uses tbname and timestamp as TDengine editable keys", () => {
]);
});
test("allows Hive table data editing even without declared primary keys", () => {
test("allows updateable SQL table data editing even without declared primary keys", () => {
assert.equal(isTableDataEditable("access", []), true);
assert.equal(isTableDataEditable("sqlite", []), true);
assert.equal(isTableDataEditable("duckdb", []), true);
@@ -54,15 +54,16 @@ test("allows Hive table data editing even without declared primary keys", () =>
assert.equal(isTableDataEditable("trino", []), true);
assert.equal(isTableDataEditable("informix", []), true);
assert.equal(isTableDataEditable("tdengine", []), true);
assert.equal(isTableDataEditable("mysql", []), false);
assert.equal(isTableDataEditable("mysql", []), true);
assert.equal(isTableDataEditable("postgres", []), true);
assert.equal(isTableDataEditable("postgres", ["id"]), true);
});
test("does not use transactional grid saves for Hive", () => {
test("does not use transactional grid saves for non-transactional engines", () => {
assert.equal(supportsDataGridTransaction("hive"), false);
assert.equal(supportsDataGridTransaction("trino"), false);
assert.equal(supportsDataGridTransaction("jdbc"), false);
assert.equal(supportsDataGridTransaction("yashandb"), false);
assert.equal(supportsDataGridTransaction("yashandb"), true);
assert.equal(supportsDataGridTransaction("postgres"), true);
});
@@ -76,11 +77,13 @@ test("allows existing row edits according to database-specific key requirements"
assert.equal(canEditExistingTableRows("hive", undefined), false);
assert.equal(canEditExistingTableRows("trino", undefined, []), false);
assert.equal(canEditExistingTableRows("trino", undefined, ["id"]), true);
assert.equal(canEditExistingTableRows("sqlite", undefined, []), false);
assert.equal(canEditExistingTableRows("mysql", undefined, []), true);
assert.equal(canEditExistingTableRows("postgres", undefined, []), true);
assert.equal(canEditExistingTableRows("sqlite", undefined, []), true);
assert.equal(canEditExistingTableRows("sqlite", undefined, ["id"]), true);
assert.equal(canEditExistingTableRows("duckdb", undefined, []), false);
assert.equal(canEditExistingTableRows("duckdb", undefined, []), true);
assert.equal(canEditExistingTableRows("duckdb", undefined, ["id"]), true);
assert.equal(canEditExistingTableRows("informix", undefined, []), false);
assert.equal(canEditExistingTableRows("informix", undefined, []), true);
assert.equal(canEditExistingTableRows("informix", undefined, ["id"]), true);
assert.equal(canEditExistingTableRows("tdengine", undefined, ["ts"]), false);
assert.equal(canEditExistingTableRows("tdengine", undefined, [DBX_TDENGINE_TBNAME_COLUMN, "ts"]), true);