Skip to content

Commit f3182f7

Browse files
zknprclaude
andcommitted
feat(deleteColumns): Add confirmation dialog for dependent indexes
When deleting columns that have dependent indexes, the extension now: - Detects indexes that reference the columns being deleted - Shows a warning dialog listing affected indexes - Asks user to confirm dropping indexes or cancel - Only proceeds if user confirms, avoiding silent database reload on cancel Added findDependentIndexes() method to both WASM and native backends. Updated LoggingDatabaseOperations wrapper to forward the new methods. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 20dc954 commit f3182f7

9 files changed

Lines changed: 193 additions & 20 deletions

File tree

core/ui/modules/crud.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,14 @@ async function submitDeleteColumns() {
202202

203203
try {
204204
updateStatus('Deleting columns...');
205-
await backendApi.deleteColumns(state.selectedTable, columnNames);
205+
const result = await backendApi.deleteColumns(state.selectedTable, columnNames);
206+
207+
// If user cancelled the operation (e.g., declined to drop dependent indexes), don't reload
208+
if (result && result.cancelled) {
209+
updateStatus('Delete cancelled');
210+
closeModal('deleteModal');
211+
return;
212+
}
206213

207214
closeModal('deleteModal');
208215
state.selectedColumns.clear();

core/ui/viewer.html

Lines changed: 4 additions & 4 deletions
Large diffs are not rendered by default.

src/core/sqlite-db.ts

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -474,14 +474,75 @@ class WasmDatabaseEngine implements DatabaseOperations {
474474
await this.executeQuery(sql, validIds);
475475
}
476476

477+
/**
478+
* Find indexes that depend on specific columns.
479+
*
480+
* @param table - Table name
481+
* @param columns - Column names to check
482+
* @returns Array of index names that reference any of the columns
483+
*/
484+
async findDependentIndexes(table: string, columns: string[]): Promise<string[]> {
485+
const dependentIndexes: string[] = [];
486+
487+
// Query sqlite_master for indexes on this table
488+
const indexQuery = `
489+
SELECT name, sql FROM sqlite_master
490+
WHERE type = 'index'
491+
AND tbl_name = ?
492+
AND sql IS NOT NULL
493+
`;
494+
const indexResult = await this.executeQuery(indexQuery, [table]);
495+
496+
if (indexResult.length > 0 && indexResult[0].rows) {
497+
for (const row of indexResult[0].rows) {
498+
const indexName = row[0] as string;
499+
const indexSql = row[1] as string;
500+
501+
// Check if this index references any of the columns
502+
const referencesColumn = columns.some(col => {
503+
const colLower = col.toLowerCase();
504+
// Match column name in index definition (quoted or unquoted)
505+
const patterns = [
506+
new RegExp(`[\\(,]\\s*${colLower}\\s*[\\),]`, 'i'),
507+
new RegExp(`[\\(,]\\s*"${colLower}"\\s*[\\),]`, 'i'),
508+
new RegExp(`[\\(,]\\s*\\[${colLower}\\]\\s*[\\),]`, 'i'),
509+
new RegExp(`[\\(,]\\s*\`${colLower}\`\\s*[\\),]`, 'i')
510+
];
511+
return patterns.some(p => p.test(indexSql));
512+
});
513+
514+
if (referencesColumn) {
515+
dependentIndexes.push(indexName);
516+
}
517+
}
518+
}
519+
520+
return dependentIndexes;
521+
}
522+
477523
/**
478524
* Delete columns by name.
525+
*
526+
* If dropDependentIndexes is provided, those indexes will be dropped first.
527+
* Otherwise, deletion may fail if indexes reference the columns.
528+
*
529+
* @param table - Table name
530+
* @param columns - Column names to delete
531+
* @param dropDependentIndexes - Optional list of indexes to drop first
479532
*/
480-
async deleteColumns(table: string, columns: string[]): Promise<void> {
533+
async deleteColumns(table: string, columns: string[], dropDependentIndexes?: string[]): Promise<void> {
481534
if (columns.length === 0) return;
482535

483536
const escapedTable = escapeIdentifier(table);
484537

538+
// Drop specified dependent indexes first
539+
if (dropDependentIndexes && dropDependentIndexes.length > 0) {
540+
for (const indexName of dropDependentIndexes) {
541+
await this.executeQuery(`DROP INDEX IF EXISTS ${escapeIdentifier(indexName)}`);
542+
}
543+
}
544+
545+
// Now drop the columns
485546
for (const col of columns) {
486547
const sql = `ALTER TABLE ${escapedTable} DROP COLUMN ${escapeIdentifier(col)}`;
487548
await this.executeQuery(sql);
@@ -967,9 +1028,14 @@ export function createWorkerEndpoint() {
9671028
return activeEngine.deleteRows(table, rowIds);
9681029
},
9691030

970-
async deleteColumns(table: string, columns: string[]): Promise<void> {
1031+
async deleteColumns(table: string, columns: string[], dropDependentIndexes?: string[]): Promise<void> {
1032+
if (!activeEngine) throw new Error('No database initialized');
1033+
return activeEngine.deleteColumns(table, columns, dropDependentIndexes);
1034+
},
1035+
1036+
async findDependentIndexes(table: string, columns: string[]): Promise<string[]> {
9711037
if (!activeEngine) throw new Error('No database initialized');
972-
return activeEngine.deleteColumns(table, columns);
1038+
return activeEngine.findDependentIndexes(table, columns);
9731039
},
9741040

9751041
async createTable(table: string, columns: ColumnDefinition[]): Promise<void> {

src/core/types.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,10 @@ export interface DatabaseOperations {
220220
deleteRows(table: string, rowIds: RecordId[]): Promise<void>;
221221

222222
/** Delete columns by name */
223-
deleteColumns(table: string, columns: string[]): Promise<void>;
223+
deleteColumns(table: string, columns: string[], dropDependentIndexes?: string[]): Promise<void>;
224+
225+
/** Find indexes that depend on specific columns */
226+
findDependentIndexes(table: string, columns: string[]): Promise<string[]>;
224227

225228
/** Create a new table */
226229
createTable(table: string, columns: ColumnDefinition[]): Promise<void>;

src/hostBridge.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,8 +250,13 @@ export class HostBridge implements ToastService {
250250

251251
/**
252252
* Delete columns.
253+
*
254+
* If columns have dependent indexes, shows a confirmation dialog to the user.
255+
* User can choose to drop the indexes and continue, or cancel the operation.
256+
*
257+
* @returns Object with `cancelled: true` if user cancelled, otherwise undefined
253258
*/
254-
async deleteColumns(table: string, columns: string[]) {
259+
async deleteColumns(table: string, columns: string[]): Promise<{ cancelled: boolean } | void> {
255260
const { document } = this;
256261
if (!document.databaseOperations) {
257262
throw new Error("Database not initialized");
@@ -261,6 +266,33 @@ export class HostBridge implements ToastService {
261266
throw new Error("Document is read-only");
262267
}
263268

269+
// Check for dependent indexes before deletion
270+
let dependentIndexes: string[] = [];
271+
if ('findDependentIndexes' in document.databaseOperations) {
272+
dependentIndexes = await document.databaseOperations.findDependentIndexes(table, columns);
273+
}
274+
275+
// If there are dependent indexes, ask the user for confirmation
276+
if (dependentIndexes.length > 0) {
277+
const indexList = dependentIndexes.join(', ');
278+
const message = vsc.l10n.t(
279+
'The following indexes depend on the selected column(s) and will be dropped: {0}',
280+
indexList
281+
);
282+
283+
const result = await vsc.window.showWarningMessage(
284+
message,
285+
{ modal: true },
286+
{ title: vsc.l10n.t('Drop Indexes & Continue'), value: true },
287+
{ title: vsc.l10n.t('Cancel'), value: false, isCloseAffordance: true }
288+
);
289+
290+
if (!result?.value) {
291+
// User cancelled the operation - return cancelled flag
292+
return { cancelled: true };
293+
}
294+
}
295+
264296
// Capture column data before deletion for undo
265297
let deletedColumnsData: { name: string; type: string; data: { rowId: RecordId; value: CellValue }[] }[] = [];
266298
try {
@@ -303,7 +335,8 @@ export class HostBridge implements ToastService {
303335
}
304336

305337
if ('deleteColumns' in document.databaseOperations) {
306-
await document.databaseOperations.deleteColumns(table, columns);
338+
// Pass dependent indexes to be dropped first if user confirmed
339+
await document.databaseOperations.deleteColumns(table, columns, dependentIndexes.length > 0 ? dependentIndexes : undefined);
307340
} else {
308341
throw new Error("Backend does not support deleteColumns");
309342
}

src/loggingDatabaseOperations.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,11 +153,21 @@ export class LoggingDatabaseOperations implements DatabaseOperations {
153153
return this.wrapped.deleteRows(table, rowIds);
154154
}
155155

156-
async deleteColumns(table: string, columns: string[]): Promise<void> {
156+
async deleteColumns(table: string, columns: string[], dropDependentIndexes?: string[]): Promise<void> {
157+
if (dropDependentIndexes && dropDependentIndexes.length > 0) {
158+
for (const indexName of dropDependentIndexes) {
159+
this.log(`DROP INDEX IF EXISTS ${escapeIdentifier(indexName)}`, true);
160+
}
161+
}
157162
for (const col of columns) {
158163
this.log(`ALTER TABLE ${escapeIdentifier(table)} DROP COLUMN ${escapeIdentifier(col)}`, true);
159164
}
160-
return this.wrapped.deleteColumns(table, columns);
165+
return this.wrapped.deleteColumns(table, columns, dropDependentIndexes);
166+
}
167+
168+
async findDependentIndexes(table: string, columns: string[]): Promise<string[]> {
169+
this.log(`Finding dependent indexes for ${escapeIdentifier(table)} columns: ${columns.join(', ')}`, false);
170+
return this.wrapped.findDependentIndexes(table, columns);
161171
}
162172

163173
async createTable(table: string, columns: ColumnDefinition[]): Promise<void> {

src/nativeWorker.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -732,14 +732,65 @@ export async function createNativeDatabaseConnection(
732732
await worker.call('run', [sql, validIds]);
733733
},
734734

735+
/**
736+
* Find indexes that depend on specific columns.
737+
*/
738+
findDependentIndexes: async (table: string, columns: string[]): Promise<string[]> => {
739+
const dependentIndexes: string[] = [];
740+
741+
// Query sqlite_master for indexes on this table
742+
const indexQuery = `
743+
SELECT name, sql FROM sqlite_master
744+
WHERE type = 'index'
745+
AND tbl_name = ?
746+
AND sql IS NOT NULL
747+
`;
748+
const indexResult = await worker.call<any>('query', [indexQuery, [table]]);
749+
750+
if (indexResult && indexResult.values) {
751+
for (const row of indexResult.values) {
752+
const indexName = row[0] as string;
753+
const indexSql = row[1] as string;
754+
755+
// Check if this index references any of the columns
756+
const referencesColumn = columns.some(col => {
757+
const colLower = col.toLowerCase();
758+
// Match column name in index definition (quoted or unquoted)
759+
const patterns = [
760+
new RegExp(`[\\(,]\\s*${colLower}\\s*[\\),]`, 'i'),
761+
new RegExp(`[\\(,]\\s*"${colLower}"\\s*[\\),]`, 'i'),
762+
new RegExp(`[\\(,]\\s*\\[${colLower}\\]\\s*[\\),]`, 'i'),
763+
new RegExp(`[\\(,]\\s*\`${colLower}\`\\s*[\\),]`, 'i')
764+
];
765+
return patterns.some(p => p.test(indexSql));
766+
});
767+
768+
if (referencesColumn) {
769+
dependentIndexes.push(indexName);
770+
}
771+
}
772+
}
773+
774+
return dependentIndexes;
775+
},
776+
735777
/**
736778
* Delete columns by name.
779+
* If dropDependentIndexes is provided, those indexes will be dropped first.
737780
*/
738-
deleteColumns: async (table: string, columns: string[]) => {
781+
deleteColumns: async (table: string, columns: string[], dropDependentIndexes?: string[]) => {
739782
if (columns.length === 0) return;
740783

741784
const escapedTable = escapeIdentifier(table);
742785

786+
// Drop specified dependent indexes first
787+
if (dropDependentIndexes && dropDependentIndexes.length > 0) {
788+
for (const indexName of dropDependentIndexes) {
789+
await worker.call('run', [`DROP INDEX IF EXISTS ${escapeIdentifier(indexName)}`]);
790+
}
791+
}
792+
793+
// Now drop the columns
743794
for (const col of columns) {
744795
const sql = `ALTER TABLE ${escapedTable} DROP COLUMN ${escapeIdentifier(col)}`;
745796
await worker.call('run', [sql]);

src/workerFactory.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ interface WorkerMethods {
9595
updateCell(table: string, rowId: string | number, column: string, value: CellValue): Promise<void>;
9696
insertRow(table: string, data: Record<string, CellValue>): Promise<string | number | undefined>;
9797
deleteRows(table: string, rowIds: (string | number)[]): Promise<void>;
98-
deleteColumns(table: string, columns: string[]): Promise<void>;
98+
deleteColumns(table: string, columns: string[], dropDependentIndexes?: string[]): Promise<void>;
99+
findDependentIndexes(table: string, columns: string[]): Promise<string[]>;
99100
createTable(table: string, columns: ColumnDefinition[]): Promise<void>;
100101
updateCellBatch(table: string, updates: CellUpdate[]): Promise<void>;
101102
addColumn(table: string, column: string, type: string, defaultValue?: string): Promise<void>;
@@ -228,7 +229,7 @@ async function createWasmDatabaseConnection(
228229
}
229230
}
230231
},
231-
['initializeDatabase', 'runQuery', 'exportDatabase', 'updateCell', 'insertRow', 'deleteRows', 'deleteColumns', 'createTable', 'updateCellBatch', 'addColumn', 'fetchTableData', 'fetchTableCount', 'fetchSchema', 'getTableInfo', 'getPragmas', 'setPragma', 'ping', 'writeToFile']
232+
['initializeDatabase', 'runQuery', 'exportDatabase', 'updateCell', 'insertRow', 'deleteRows', 'deleteColumns', 'findDependentIndexes', 'createTable', 'updateCellBatch', 'addColumn', 'fetchTableData', 'fetchTableCount', 'fetchSchema', 'getTableInfo', 'getPragmas', 'setPragma', 'ping', 'writeToFile']
232233
);
233234

234235
// Termination handler
@@ -347,8 +348,10 @@ async function createWasmDatabaseConnection(
347348
},
348349
deleteRows: (table: string, rowIds: (string | number)[]) =>
349350
workerProxy.deleteRows(table, rowIds),
350-
deleteColumns: (table: string, columns: string[]) =>
351-
workerProxy.deleteColumns(table, columns),
351+
deleteColumns: (table: string, columns: string[], dropDependentIndexes?: string[]) =>
352+
workerProxy.deleteColumns(table, columns, dropDependentIndexes),
353+
findDependentIndexes: (table: string, columns: string[]) =>
354+
workerProxy.findDependentIndexes(table, columns),
352355
createTable: (table: string, columns: ColumnDefinition[]) =>
353356
workerProxy.createTable(table, columns),
354357
updateCellBatch: (table: string, updates: CellUpdate[]) => {

website/public/sqlite-viewer/viewer.html

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)