diff --git a/apps/desktop/src/composables/useDataGridExport.ts b/apps/desktop/src/composables/useDataGridExport.ts index 710542be75..2328f4d00c 100644 --- a/apps/desktop/src/composables/useDataGridExport.ts +++ b/apps/desktop/src/composables/useDataGridExport.ts @@ -607,9 +607,7 @@ export function useDataGridExport(options: UseDataGridExportOptions) { if (!tableMeta.value?.primaryKeys.length) return false; const rows = insertEligibleRows(); if (!rows.length) return false; - const insertableCount = insertableCopyColumnCount(false); - const insertColumnsCount = insertableCopyColumnCount(true); - return insertColumnsCount > 0 && insertColumnsCount < insertableCount; + return insertableCopyColumnCount(true) > 0; }); async function copyAll() { diff --git a/crates/dbx-core/src/data_grid_sql.rs b/crates/dbx-core/src/data_grid_sql.rs index 9b8ee77e79..f22c9c0f11 100644 --- a/crates/dbx-core/src/data_grid_sql.rs +++ b/crates/dbx-core/src/data_grid_sql.rs @@ -345,9 +345,6 @@ pub fn build_data_grid_copy_insert_statement(options: DataGridCopyInsertStatemen }) .collect(); - if options.exclude_primary_keys && insert_columns.len() == insertable_columns.len() { - return None; - } if insert_columns.is_empty() || options.rows.is_empty() { return None; } @@ -2195,6 +2192,31 @@ mod tests { ); } + #[test] + fn builds_copy_insert_without_primary_keys_when_primary_keys_are_hidden() { + let statement = build_data_grid_copy_insert_statement(DataGridCopyInsertStatementOptions { + database_type: Some(DatabaseType::Mysql), + table_meta: Some(DataGridTableMeta { + catalog: None, + schema: None, + table_name: "users".to_string(), + primary_keys: vec!["id".to_string()], + columns: None, + }), + columns: vec!["login_name".to_string(), "display_name".to_string()], + column_types: None, + source_columns: None, + rows: vec![vec![json!("ada"), json!("Ada")]], + exclude_primary_keys: true, + insert_mode: DataGridCopyInsertMode::Merged, + }); + + assert_eq!( + statement.as_deref(), + Some("INSERT INTO `users` (`login_name`, `display_name`) VALUES ('ada', 'Ada');") + ); + } + #[test] fn builds_copy_insert_statement_row_by_row() { let statement = build_data_grid_copy_insert_statement(DataGridCopyInsertStatementOptions { diff --git a/packages/app-tests/useDataGridExport.test.ts b/packages/app-tests/useDataGridExport.test.ts index a8f354ed52..a358153678 100644 --- a/packages/app-tests/useDataGridExport.test.ts +++ b/packages/app-tests/useDataGridExport.test.ts @@ -481,6 +481,116 @@ test("copy row as INSERT works without prior prefetch (first context-menu click) assert.equal(composable.canCopyPreparedInsert(false), true); }); +test("copy row as INSERT without primary keys works without prior prefetch", async () => { + const contextCell = ref({ rowId: 1, rowIndex: 0, col: 0 }); + const row = { + id: 1, + data: [1, "alice"], + isNew: false, + isDeleted: false, + isDirtyCol: [false, false], + status: "", + }; + apiMock.buildDataGridCopyInsertStatement.mockResolvedValueOnce("INSERT INTO users (name) VALUES ('alice');"); + const composable = useDataGridExport({ + columns: computed(() => ["id", "name"]), + displayItems: computed(() => [row]), + sql: computed(() => undefined), + tableMeta: computed(() => ({ + tableName: "users", + primaryKeys: ["id"], + })), + databaseType: computed(() => "mysql"), + connectionId: computed(() => "conn-1"), + database: computed(() => "db"), + context: computed(() => "table-data"), + sourceColumns: computed(() => undefined), + columnTypes: computed(() => undefined), + whereInput: computed(() => undefined), + orderBy: computed(() => undefined), + exportBatchSize: computed(() => 1000), + hasCellSelection: computed(() => false), + selectedCells: computed(() => ({ columns: [], rows: [] })), + selectedRange: computed(() => null), + contextCell, + getRowItem: () => row, + selectedRowIds: ref(new Set()), + hasRowSelection: computed(() => false), + }); + + assert.equal(composable.canCopyRowAsInsertWithoutPrimaryKeys.value, true); + assert.equal(composable.canCopyPreparedInsert(true), false); + + await composable.copyRowAsInsertWithoutPrimaryKeys(); + + assert.deepEqual(apiMock.buildDataGridCopyInsertStatement.mock.calls[0][0], { + databaseType: "mysql", + tableMeta: { tableName: "users", primaryKeys: ["id"] }, + columns: ["id", "name"], + columnTypes: undefined, + sourceColumns: undefined, + rows: [[1, "alice"]], + excludePrimaryKeys: true, + insertMode: "merged", + }); + assert.equal(clipboardMock.copyToClipboard.mock.calls[0][0], "INSERT INTO users (name) VALUES ('alice');"); + assert.equal(composable.canCopyPreparedInsert(true), true); +}); + +test("copy row as INSERT without primary keys remains available when the primary key is hidden", async () => { + const contextCell = ref({ rowId: 1, rowIndex: 0, col: 0 }); + const row = { + id: 1, + data: ["alice", "active"], + isNew: false, + isDeleted: false, + isDirtyCol: [false, false], + status: "", + }; + apiMock.buildDataGridCopyInsertStatement.mockResolvedValueOnce("INSERT INTO users (name, status) VALUES ('alice', 'active');"); + const composable = useDataGridExport({ + columns: computed(() => ["name", "status"]), + displayItems: computed(() => [row]), + sql: computed(() => "SELECT name, status FROM users"), + tableMeta: computed(() => ({ + tableName: "users", + primaryKeys: ["id"], + })), + databaseType: computed(() => "mysql"), + connectionId: computed(() => "conn-1"), + database: computed(() => "db"), + context: computed(() => "results"), + sourceColumns: computed(() => ["name", "status"]), + columnTypes: computed(() => undefined), + whereInput: computed(() => undefined), + orderBy: computed(() => undefined), + exportBatchSize: computed(() => 1000), + hasCellSelection: computed(() => false), + selectedCells: computed(() => ({ columns: [], rows: [] })), + selectedRange: computed(() => null), + contextCell, + getRowItem: () => row, + selectedRowIds: ref(new Set()), + hasRowSelection: computed(() => false), + }); + + assert.equal(composable.canCopyRowAsInsertWithoutPrimaryKeys.value, true); + + await composable.copyRowAsInsertWithoutPrimaryKeys(); + + assert.deepEqual(apiMock.buildDataGridCopyInsertStatement.mock.calls[0][0], { + databaseType: "mysql", + tableMeta: { tableName: "users", primaryKeys: ["id"] }, + columns: ["name", "status"], + columnTypes: undefined, + sourceColumns: ["name", "status"], + rows: [["alice", "active"]], + excludePrimaryKeys: true, + insertMode: "merged", + }); + assert.equal(clipboardMock.copyToClipboard.mock.calls[0][0], "INSERT INTO users (name, status) VALUES ('alice', 'active');"); +}); + test("default data grid export file names use sanitized base names and compact local timestamps", () => { vi.useFakeTimers(); try {