diff --git a/apps/desktop/src/lib/databaseTableDataCapabilities.ts b/apps/desktop/src/lib/databaseTableDataCapabilities.ts index 8a6c8d837e..42dd9e5ec3 100644 --- a/apps/desktop/src/lib/databaseTableDataCapabilities.ts +++ b/apps/desktop/src/lib/databaseTableDataCapabilities.ts @@ -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([ + "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>> = { - 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 { + 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", diff --git a/packages/app-tests/databaseCapabilities.test.ts b/packages/app-tests/databaseCapabilities.test.ts index 7515d6b9b2..207fdc9fcc 100644 --- a/packages/app-tests/databaseCapabilities.test.ts +++ b/packages/app-tests/databaseCapabilities.test.ts @@ -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, diff --git a/packages/app-tests/tableEditing.test.ts b/packages/app-tests/tableEditing.test.ts index 6cc45220b7..4bcc048114 100644 --- a/packages/app-tests/tableEditing.test.ts +++ b/packages/app-tests/tableEditing.test.ts @@ -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);