diff --git a/frontend/src/components/DataGrid.ddl.test.tsx b/frontend/src/components/DataGrid.ddl.test.tsx index 3a96c71..7625aff 100644 --- a/frontend/src/components/DataGrid.ddl.test.tsx +++ b/frontend/src/components/DataGrid.ddl.test.tsx @@ -8,7 +8,7 @@ import DataGrid, { GONAVI_ROW_KEY, hasDataGridVirtualEditRenderVersionChanged, } from './DataGrid'; -import { ORACLE_ROWID_LOCATOR_COLUMN } from '../utils/rowLocator'; +import { DUCKDB_ROWID_LOCATOR_COLUMN, ORACLE_ROWID_LOCATOR_COLUMN } from '../utils/rowLocator'; const storeState = vi.hoisted(() => ({ connections: [ @@ -415,6 +415,37 @@ describe('DataGrid commit change set', () => { }); }); + it('uses hidden DuckDB rowid only as locator and excludes it from update values', () => { + const result = buildDataGridCommitChangeSet({ + addedRows: [], + modifiedRows: { + 'row-1': { [GONAVI_ROW_KEY]: 'row-1', NAME: 'new-name', [DUCKDB_ROWID_LOCATOR_COLUMN]: 18 }, + }, + deletedRowKeys: new Set(), + data: [{ [GONAVI_ROW_KEY]: 'row-1', NAME: 'old-name', [DUCKDB_ROWID_LOCATOR_COLUMN]: 17 }], + editLocator: { + strategy: 'duckdb-rowid', + columns: ['rowid'], + valueColumns: [DUCKDB_ROWID_LOCATOR_COLUMN], + hiddenColumns: [DUCKDB_ROWID_LOCATOR_COLUMN], + readOnly: false, + }, + visibleColumnNames: ['NAME'], + rowKeyToString, + normalizeCommitCellValue: normalizeValue, + shouldCommitColumn: commitColumnGuard, + }); + + expect(result).toEqual({ + ok: true, + changes: { + inserts: [], + updates: [{ keys: { rowid: 17 }, values: { NAME: 'new-name' } }], + deletes: [], + }, + }); + }); + it('commits only writable result columns and maps aliases back to table columns', () => { const result = buildDataGridCommitChangeSet({ addedRows: [], diff --git a/frontend/src/components/DataViewer.primary-key.test.tsx b/frontend/src/components/DataViewer.primary-key.test.tsx index cf5fdc3..7bbd8c0 100644 --- a/frontend/src/components/DataViewer.primary-key.test.tsx +++ b/frontend/src/components/DataViewer.primary-key.test.tsx @@ -4,7 +4,7 @@ import { act, create, type ReactTestRenderer } from 'react-test-renderer'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { TabData } from '../types'; -import { ORACLE_ROWID_LOCATOR_COLUMN } from '../utils/rowLocator'; +import { DUCKDB_ROWID_LOCATOR_COLUMN, ORACLE_ROWID_LOCATOR_COLUMN } from '../utils/rowLocator'; import DataViewer from './DataViewer'; const storeState = vi.hoisted(() => ({ @@ -207,6 +207,39 @@ describe('DataViewer safe editing locator', () => { renderer.unmount(); }); + it('uses hidden DuckDB rowid when no primary or unique key is available', async () => { + storeState.connections[0].config.type = 'duckdb'; + storeState.connections[0].config.database = 'main'; + backendApp.DBGetColumns.mockResolvedValue({ + success: true, + data: [{ name: 'name', key: '' }], + }); + backendApp.DBGetIndexes.mockResolvedValue({ + success: true, + data: [], + }); + backendApp.DBQuery.mockResolvedValue({ + success: true, + fields: ['name', DUCKDB_ROWID_LOCATOR_COLUMN], + data: [{ name: 'launch', [DUCKDB_ROWID_LOCATOR_COLUMN]: 17 }], + }); + + const renderer = await renderAndReload(createTab({ id: 'tab-duckdb-rowid', dbName: 'main', tableName: 'main.events', title: 'events' })); + + expect(dataGridState.latestProps?.pkColumns).toEqual([]); + expect(dataGridState.latestProps?.editLocator).toMatchObject({ + strategy: 'duckdb-rowid', + columns: ['rowid'], + valueColumns: [DUCKDB_ROWID_LOCATOR_COLUMN], + hiddenColumns: [DUCKDB_ROWID_LOCATOR_COLUMN], + readOnly: false, + }); + expect(dataGridState.latestProps?.readOnly).toBe(false); + expect(messageApi.warning).not.toHaveBeenCalled(); + expect(backendApp.DBQuery.mock.calls.some((call: any[]) => String(call[2]).includes(`rowid AS "${DUCKDB_ROWID_LOCATOR_COLUMN}"`))).toBe(true); + renderer.unmount(); + }); + it('enables MongoDB table preview editing through the _id locator', async () => { storeState.connections[0].config.type = 'mongodb'; storeState.connections[0].config.database = 'app'; diff --git a/frontend/src/components/DataViewer.tsx b/frontend/src/components/DataViewer.tsx index 6bbc59a..bbe5817 100644 --- a/frontend/src/components/DataViewer.tsx +++ b/frontend/src/components/DataViewer.tsx @@ -16,6 +16,7 @@ import { validateQuickWhereCondition, } from '../utils/dataGridWhereFilter'; import { + DUCKDB_ROWID_LOCATOR_COLUMN, ORACLE_ROWID_LOCATOR_COLUMN, resolveEditRowLocator, type EditRowLocator, @@ -163,13 +164,18 @@ const buildDataViewerBaseSelectSQL = ( locator?: EditRowLocator, ): string => { const quotedTableName = quoteQualifiedIdent(dbType, tableName); - if (locator?.strategy !== 'oracle-rowid') { + if (locator?.strategy !== 'oracle-rowid' && locator?.strategy !== 'duckdb-rowid') { return `SELECT * FROM ${quotedTableName} ${whereSQL}`; } const alias = 'gonavi_row_source'; - const rowIDAlias = quoteIdentPart(dbType, ORACLE_ROWID_LOCATOR_COLUMN); - return `SELECT ${alias}.*, ${alias}.ROWID AS ${rowIDAlias} FROM ${quotedTableName} ${alias} ${whereSQL}`; + if (locator?.strategy === 'duckdb-rowid') { + const duckdbRowIDAlias = quoteIdentPart(dbType, DUCKDB_ROWID_LOCATOR_COLUMN); + return `SELECT ${alias}.*, ${alias}.rowid AS ${duckdbRowIDAlias} FROM ${quotedTableName} ${alias} ${whereSQL}`; + } + + const oracleRowIDAlias = quoteIdentPart(dbType, ORACLE_ROWID_LOCATOR_COLUMN); + return `SELECT ${alias}.*, ${alias}.ROWID AS ${oracleRowIDAlias} FROM ${quotedTableName} ${alias} ${whereSQL}`; }; const resolveDuckDBSchemaAndTable = (dbName: string, tableName: string) => { @@ -575,13 +581,16 @@ const DataViewer: React.FC<{ tab: TabData; isActive?: boolean }> = React.memo(({ const resultColumns = getTableColumnNames(columnDefs); const locatorColumns = isOracleLikeDialect(dbType) ? [...resultColumns, ORACLE_ROWID_LOCATOR_COLUMN] - : resultColumns; + : (String(dbType || '').trim().toLowerCase() === 'duckdb' + ? [...resultColumns, DUCKDB_ROWID_LOCATOR_COLUMN] + : resultColumns); let nextLocator = resolveEditRowLocator({ dbType, resultColumns: locatorColumns, primaryKeys, indexes, allowOracleRowID: true, + allowDuckDBRowID: String(dbType || '').trim().toLowerCase() === 'duckdb', }); if (nextLocator.readOnly && primaryKeys.length === 0 && !resIndexes?.success && !isOracleLikeDialect(dbType)) { diff --git a/frontend/src/components/QueryEditor.external-sql-save.test.tsx b/frontend/src/components/QueryEditor.external-sql-save.test.tsx index 5166d13..2b05f95 100644 --- a/frontend/src/components/QueryEditor.external-sql-save.test.tsx +++ b/frontend/src/components/QueryEditor.external-sql-save.test.tsx @@ -3387,6 +3387,52 @@ describe('QueryEditor external SQL save', () => { expect(messageApi.warning).not.toHaveBeenCalled(); }); + it('uses hidden DuckDB rowid when query results have no primary or unique key', async () => { + storeState.connections[0].config.type = 'duckdb'; + storeState.connections[0].config.database = 'main'; + backendApp.DBQueryMulti.mockResolvedValueOnce({ + success: true, + data: [{ columns: ['NAME', '__gonavi_duckdb_rowid__'], rows: [{ NAME: 'launch', __gonavi_duckdb_rowid__: 17 }] }], + }); + backendApp.DBGetColumns.mockResolvedValueOnce({ + success: true, + data: [{ name: 'name', key: '' }], + }); + backendApp.DBGetIndexes.mockResolvedValueOnce({ + success: true, + data: [], + }); + + let renderer: ReactTestRenderer; + await act(async () => { + renderer = create(); + }); + + await act(async () => { + await findButton(renderer!, '运行').props.onClick(); + }); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(dataGridState.latestProps?.tableName).toBe('main.events'); + expect(dataGridState.latestProps?.pkColumns).toEqual([]); + expect(dataGridState.latestProps?.editLocator).toMatchObject({ + strategy: 'duckdb-rowid', + columns: ['rowid'], + valueColumns: ['__gonavi_duckdb_rowid__'], + hiddenColumns: ['__gonavi_duckdb_rowid__'], + writableColumns: { + NAME: 'name', + }, + readOnly: false, + }); + expect(dataGridState.latestProps?.readOnly).toBe(false); + expect(String(backendApp.DBQueryMulti.mock.calls[0][2])).toContain('rowid AS "__gonavi_duckdb_rowid__"'); + expect(messageApi.warning).not.toHaveBeenCalled(); + }); + it.each([ 'mysql', 'mariadb', diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx index c5190ea..ca4d316 100644 --- a/frontend/src/components/QueryEditor.tsx +++ b/frontend/src/components/QueryEditor.tsx @@ -24,7 +24,11 @@ import { splitSidebarQualifiedName } from '../utils/sidebarLocate'; import { normalizeSidebarViewName } from '../utils/sidebarMetadata'; import { SIDEBAR_SQL_EDITOR_DRAG_MIME, decodeSidebarSqlEditorDragPayload, hasSidebarSqlEditorDragPayload } from '../utils/sidebarSqlDrag'; import { resolveUniqueKeyGroupsFromIndexes } from './dataGridCopyInsert'; -import { ORACLE_ROWID_LOCATOR_COLUMN, type EditRowLocator } from '../utils/rowLocator'; +import { + DUCKDB_ROWID_LOCATOR_COLUMN, + ORACLE_ROWID_LOCATOR_COLUMN, + type EditRowLocator, +} from '../utils/rowLocator'; import { getQueryTabDraft, hasQueryTabDraft, setQueryTabDraft, setSQLFileTabDraft } from '../utils/sqlFileTabDrafts'; import { getColumnDefinitionKey, @@ -579,6 +583,10 @@ const buildQueryRowIDExpression = (dbType: string, sourceAlias?: string): string `${sourceAlias ? `${sourceAlias}.` : ''}ROWID AS ${quoteIdentPart(dbType, ORACLE_ROWID_LOCATOR_COLUMN)}` ); +const buildDuckDBRowIDExpression = (dbType: string, sourceAlias?: string): string => ( + `${sourceAlias ? `${sourceAlias}.` : ''}rowid AS ${quoteIdentPart(dbType, DUCKDB_ROWID_LOCATOR_COLUMN)}` +); + const escapeMetadataSqlLiteral = (raw: string): string => String(raw || '').replace(/'/g, "''"); const quoteSqlServerDbIdentifier = (raw: string): string => `[${String(raw || '').replace(/]/g, ']]')}]`; @@ -1834,6 +1842,7 @@ const resolveQueryLocatorPlan = async ({ const appendExpressions: string[] = []; const hiddenColumns: string[] = []; let needsOracleRowIDExpression = false; + let needsDuckDBRowIDExpression = false; const buildColumnLocator = (strategy: 'primary-key' | 'unique-key', locatorColumns: string[]): EditRowLocator => { const valueColumns = locatorColumns.map((column, index) => { @@ -1872,6 +1881,16 @@ const resolveQueryLocatorPlan = async ({ writableColumns, readOnly: false, }; + } else if (String(dbType || '').trim().toLowerCase() === 'duckdb') { + needsDuckDBRowIDExpression = true; + plan.editLocator = { + strategy: 'duckdb-rowid', + columns: ['rowid'], + valueColumns: [DUCKDB_ROWID_LOCATOR_COLUMN], + hiddenColumns: [DUCKDB_ROWID_LOCATOR_COLUMN], + writableColumns, + readOnly: false, + }; } else { const reason = !resIndexes?.success ? '无法加载唯一索引元数据,无法安全提交修改。' @@ -1883,6 +1902,7 @@ const resolveQueryLocatorPlan = async ({ const executableAppendExpressions = [ ...(needsOracleRowIDExpression ? [buildQueryRowIDExpression(dbType)] : []), + ...(needsDuckDBRowIDExpression ? [buildDuckDBRowIDExpression(dbType)] : []), ...appendExpressions, ]; diff --git a/frontend/src/utils/rowLocator.test.ts b/frontend/src/utils/rowLocator.test.ts index 5c0916d..d9e28d2 100644 --- a/frontend/src/utils/rowLocator.test.ts +++ b/frontend/src/utils/rowLocator.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; import { + DUCKDB_ROWID_LOCATOR_COLUMN, ORACLE_ROWID_LOCATOR_COLUMN, filterHiddenLocatorColumns, resolveEditRowLocator, @@ -103,6 +104,20 @@ describe('resolveEditRowLocator', () => { readOnly: false, }); }); + + it('uses DuckDB rowid when no primary or unique key is available', () => { + expect(resolveEditRowLocator({ + dbType: 'duckdb', + resultColumns: ['name', DUCKDB_ROWID_LOCATOR_COLUMN], + allowDuckDBRowID: true, + })).toEqual({ + strategy: 'duckdb-rowid', + columns: ['rowid'], + valueColumns: [DUCKDB_ROWID_LOCATOR_COLUMN], + hiddenColumns: [DUCKDB_ROWID_LOCATOR_COLUMN], + readOnly: false, + }); + }); }); describe('resolveRowLocatorValues', () => { @@ -131,6 +146,19 @@ describe('resolveRowLocatorValues', () => { error: '定位列 EMAIL 的值为空,无法安全提交修改。', }); }); + + it('extracts DuckDB rowid locator values from the original row', () => { + const locator = resolveEditRowLocator({ + dbType: 'duckdb', + resultColumns: ['name', DUCKDB_ROWID_LOCATOR_COLUMN], + allowDuckDBRowID: true, + }); + + expect(resolveRowLocatorValues(locator, { name: 'launch', [DUCKDB_ROWID_LOCATOR_COLUMN]: 17 })).toEqual({ + ok: true, + values: { rowid: 17 }, + }); + }); }); describe('filterHiddenLocatorColumns', () => { @@ -143,4 +171,14 @@ describe('filterHiddenLocatorColumns', () => { expect(filterHiddenLocatorColumns(['NAME', ORACLE_ROWID_LOCATOR_COLUMN], locator)).toEqual(['NAME']); }); + + it('removes hidden DuckDB rowid columns from displayed columns', () => { + const locator = resolveEditRowLocator({ + dbType: 'duckdb', + resultColumns: ['name', DUCKDB_ROWID_LOCATOR_COLUMN], + allowDuckDBRowID: true, + }); + + expect(filterHiddenLocatorColumns(['name', DUCKDB_ROWID_LOCATOR_COLUMN], locator)).toEqual(['name']); + }); }); diff --git a/frontend/src/utils/rowLocator.ts b/frontend/src/utils/rowLocator.ts index 7196d47..d2a6f11 100644 --- a/frontend/src/utils/rowLocator.ts +++ b/frontend/src/utils/rowLocator.ts @@ -3,8 +3,9 @@ import { resolveUniqueKeyGroupsFromIndexes } from '../components/dataGridCopyIns import { isOracleLikeDialect } from './sqlDialect'; export const ORACLE_ROWID_LOCATOR_COLUMN = '__gonavi_oracle_rowid__'; +export const DUCKDB_ROWID_LOCATOR_COLUMN = '__gonavi_duckdb_rowid__'; -export type RowLocatorStrategy = 'primary-key' | 'unique-key' | 'oracle-rowid' | 'none'; +export type RowLocatorStrategy = 'primary-key' | 'unique-key' | 'oracle-rowid' | 'duckdb-rowid' | 'none'; export type EditRowLocator = { strategy: RowLocatorStrategy; @@ -22,6 +23,7 @@ export type ResolveEditRowLocatorParams = { primaryKeys?: string[]; indexes?: IndexDefinition[]; allowOracleRowID?: boolean; + allowDuckDBRowID?: boolean; }; export type ResolveRowLocatorValuesResult = @@ -54,6 +56,7 @@ export const resolveEditRowLocator = ({ primaryKeys = [], indexes, allowOracleRowID = false, + allowDuckDBRowID = false, }: ResolveEditRowLocatorParams): EditRowLocator => { const columns = (resultColumns || []).map(normalizeColumnName).filter(Boolean); const primaryKeyColumns = (primaryKeys || []).map(normalizeColumnName).filter(Boolean); @@ -93,10 +96,25 @@ export const resolveEditRowLocator = ({ }; } + if (allowDuckDBRowID && String(dbType || '').trim().toLowerCase() === 'duckdb' && hasColumn(columns, DUCKDB_ROWID_LOCATOR_COLUMN)) { + const rowIDColumn = findColumn(columns, DUCKDB_ROWID_LOCATOR_COLUMN); + return { + strategy: 'duckdb-rowid', + columns: ['rowid'], + valueColumns: [rowIDColumn], + hiddenColumns: [rowIDColumn], + readOnly: false, + }; + } + if (allowOracleRowID && isOracleLikeDialect(dbType)) { return buildReadOnlyLocator('未检测到主键或可用唯一索引,且结果中缺少 Oracle ROWID,无法安全提交修改。'); } + if (allowDuckDBRowID && String(dbType || '').trim().toLowerCase() === 'duckdb') { + return buildReadOnlyLocator('未检测到主键、可用唯一索引或 DuckDB rowid,无法安全提交修改。'); + } + return buildReadOnlyLocator('未检测到主键或可用唯一索引,无法安全提交修改。'); }; diff --git a/internal/db/duckdb_applychanges_test.go b/internal/db/duckdb_applychanges_test.go new file mode 100644 index 0000000..65a8d67 --- /dev/null +++ b/internal/db/duckdb_applychanges_test.go @@ -0,0 +1,160 @@ +//go:build gonavi_full_drivers || gonavi_duckdb_driver + +package db + +import ( + "context" + "database/sql" + "database/sql/driver" + "fmt" + "sync" + "testing" + + "GoNavi-Wails/internal/connection" +) + +const duckdbRecordingDriverName = "gonavi_duckdb_recording" + +var ( + registerDuckDBRecordingDriverOnce sync.Once + duckdbRecordingDriverMu sync.Mutex + duckdbRecordingDriverSeq int + duckdbRecordingDriverStates = map[string]*duckdbRecordingState{} +) + +type duckdbRecordingState struct { + mu sync.Mutex + execQueries []string + execArgs [][]driver.NamedValue +} + +func (s *duckdbRecordingState) snapshotExecQueries() []string { + s.mu.Lock() + defer s.mu.Unlock() + return append([]string(nil), s.execQueries...) +} + +func (s *duckdbRecordingState) snapshotExecArgs() [][]driver.NamedValue { + s.mu.Lock() + defer s.mu.Unlock() + result := make([][]driver.NamedValue, len(s.execArgs)) + for i, args := range s.execArgs { + result[i] = append([]driver.NamedValue(nil), args...) + } + return result +} + +type duckdbRecordingDriver struct{} + +func (duckdbRecordingDriver) Open(name string) (driver.Conn, error) { + duckdbRecordingDriverMu.Lock() + state := duckdbRecordingDriverStates[name] + duckdbRecordingDriverMu.Unlock() + if state == nil { + return nil, fmt.Errorf("recording state not found: %s", name) + } + return &duckdbRecordingConn{state: state}, nil +} + +type duckdbRecordingConn struct { + state *duckdbRecordingState +} + +func (c *duckdbRecordingConn) Prepare(query string) (driver.Stmt, error) { + return nil, fmt.Errorf("prepare not supported in duckdb recording driver: %s", query) +} + +func (c *duckdbRecordingConn) Close() error { return nil } + +func (c *duckdbRecordingConn) Begin() (driver.Tx, error) { return duckdbRecordingTx{}, nil } + +func (c *duckdbRecordingConn) ExecContext(_ context.Context, query string, args []driver.NamedValue) (driver.Result, error) { + c.state.mu.Lock() + defer c.state.mu.Unlock() + c.state.execQueries = append(c.state.execQueries, query) + c.state.execArgs = append(c.state.execArgs, append([]driver.NamedValue(nil), args...)) + return driver.RowsAffected(1), nil +} + +var _ driver.ExecerContext = (*duckdbRecordingConn)(nil) + +type duckdbRecordingTx struct{} + +func (duckdbRecordingTx) Commit() error { return nil } +func (duckdbRecordingTx) Rollback() error { return nil } + +func openDuckDBRecordingDB(t *testing.T) (*sql.DB, *duckdbRecordingState) { + t.Helper() + registerDuckDBRecordingDriverOnce.Do(func() { + sql.Register(duckdbRecordingDriverName, duckdbRecordingDriver{}) + }) + + duckdbRecordingDriverMu.Lock() + duckdbRecordingDriverSeq++ + dsn := fmt.Sprintf("duckdb-recording-%d", duckdbRecordingDriverSeq) + state := &duckdbRecordingState{} + duckdbRecordingDriverStates[dsn] = state + duckdbRecordingDriverMu.Unlock() + + dbConn, err := sql.Open(duckdbRecordingDriverName, dsn) + if err != nil { + t.Fatalf("打开 duckdb recording db 失败: %v", err) + } + + t.Cleanup(func() { + _ = dbConn.Close() + duckdbRecordingDriverMu.Lock() + delete(duckdbRecordingDriverStates, dsn) + duckdbRecordingDriverMu.Unlock() + }) + + return dbConn, state +} + +func TestDuckDBApplyChangesUsesUnquotedRowIDLocator(t *testing.T) { + t.Parallel() + + dbConn, state := openDuckDBRecordingDB(t) + duckdb := &DuckDB{conn: dbConn} + + changes := connection.ChangeSet{ + Updates: []connection.UpdateRow{{ + Keys: map[string]interface{}{ + "rowid": 17, + }, + Values: map[string]interface{}{ + "name": "renamed", + }, + }}, + Deletes: []map[string]interface{}{ + {"rowid": 21}, + }, + LocatorStrategy: "duckdb-rowid", + } + + if err := duckdb.ApplyChanges("main.events", changes); err != nil { + t.Fatalf("ApplyChanges 返回错误: %v", err) + } + + queries := state.snapshotExecQueries() + if len(queries) != 2 { + t.Fatalf("期望执行 2 条 SQL,实际=%d %#v", len(queries), queries) + } + if queries[0] != `DELETE FROM "main"."events" WHERE rowid = ?` { + t.Fatalf("删除 SQL 不符合预期: %s", queries[0]) + } + if queries[1] != `UPDATE "main"."events" SET "name" = ? WHERE rowid = ?` { + t.Fatalf("更新 SQL 不符合预期: %s", queries[1]) + } + + args := state.snapshotExecArgs() + if len(args) != 2 || len(args[0]) != 1 || len(args[1]) != 2 { + t.Fatalf("执行参数数量不符合预期: %#v", args) + } + if args[0][0].Value != 21 { + t.Fatalf("删除 rowid 参数错误: %#v", args[0]) + } + if args[1][0].Value != "renamed" || args[1][1].Value != 17 { + t.Fatalf("更新参数错误: %#v", args[1]) + } +} diff --git a/internal/db/duckdb_impl.go b/internal/db/duckdb_impl.go index 82d6e6b..008e8bd 100644 --- a/internal/db/duckdb_impl.go +++ b/internal/db/duckdb_impl.go @@ -423,13 +423,24 @@ func (d *DuckDB) ApplyChanges(tableName string, changes connection.ChangeSet) er qualifiedTable = fmt.Sprintf("%s.%s", quoteIdent(schema), quoteIdent(table)) } - for _, pk := range changes.Deletes { + isDuckDBRowIDLocator := strings.EqualFold(strings.TrimSpace(changes.LocatorStrategy), "duckdb-rowid") + buildWhere := func(keys map[string]interface{}) ([]string, []interface{}) { var wheres []string var args []interface{} - for k, v := range pk { + for k, v := range keys { + if isDuckDBRowIDLocator && strings.EqualFold(strings.TrimSpace(k), "rowid") { + wheres = append(wheres, "rowid = ?") + args = append(args, v) + continue + } wheres = append(wheres, fmt.Sprintf("%s = ?", quoteIdent(k))) args = append(args, v) } + return wheres, args + } + + for _, pk := range changes.Deletes { + wheres, args := buildWhere(pk) if len(wheres) == 0 { continue } @@ -450,11 +461,8 @@ func (d *DuckDB) ApplyChanges(tableName string, changes connection.ChangeSet) er continue } - var wheres []string - for k, v := range update.Keys { - wheres = append(wheres, fmt.Sprintf("%s = ?", quoteIdent(k))) - args = append(args, v) - } + wheres, whereArgs := buildWhere(update.Keys) + args = append(args, whereArgs...) if len(wheres) == 0 { return fmt.Errorf("更新操作需要主键条件") }