@rozenite/sqlite-plugin 1.13.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/devtools/assets/panel-BarPLQ4-.css +1 -0
  3. package/dist/devtools/assets/{panel-DjRs5NTl.js → panel-BsdCagVv.js} +45 -44
  4. package/dist/devtools/panel.html +2 -2
  5. package/dist/react-native/chunks/index.require.js +4 -16
  6. package/dist/react-native/chunks/sql.require.js +1 -3
  7. package/dist/react-native/chunks/useRozeniteSqlitePlugin.require.js +92 -110
  8. package/dist/react-native/index.d.ts +2 -2
  9. package/dist/rozenite.json +1 -1
  10. package/package.json +27 -31
  11. package/react-native.ts +7 -14
  12. package/src/react-native/adapters/__tests__/expo-sqlite.test.ts +4 -14
  13. package/src/react-native/adapters/expo-sqlite.ts +11 -40
  14. package/src/react-native/adapters/generic.ts +5 -13
  15. package/src/react-native/adapters/index.ts +1 -4
  16. package/src/react-native/sqlite-view.ts +2 -8
  17. package/src/react-native/useRozeniteSqlitePlugin.ts +110 -130
  18. package/src/react-native/useSqliteAgentTools.ts +5 -14
  19. package/src/shared/__tests__/bridge-values.test.ts +6 -11
  20. package/src/shared/__tests__/sql.test.ts +4 -8
  21. package/src/shared/bridge-values.ts +2 -5
  22. package/src/shared/sql.ts +3 -7
  23. package/src/ui/__tests__/sql-editor-utils.test.ts +1 -5
  24. package/src/ui/__tests__/sqlite-drop-mutations.test.ts +138 -0
  25. package/src/ui/__tests__/sqlite-row-mutations.test.ts +1 -3
  26. package/src/ui/__tests__/sqlite-table-column-order.test.ts +4 -19
  27. package/src/ui/cell-detail-drawer.tsx +1 -6
  28. package/src/ui/globals.css +26 -65
  29. package/src/ui/panel.tsx +442 -568
  30. package/src/ui/query-result-table.tsx +8 -26
  31. package/src/ui/sql-editor-utils.ts +24 -55
  32. package/src/ui/sql-editor.tsx +13 -46
  33. package/src/ui/sqlite-data-table.tsx +11 -28
  34. package/src/ui/sqlite-drop-modal.tsx +193 -0
  35. package/src/ui/sqlite-drop-mutations.ts +53 -0
  36. package/src/ui/sqlite-introspection.ts +9 -25
  37. package/src/ui/sqlite-row-delete-modal.tsx +5 -12
  38. package/src/ui/sqlite-row-edit-modal.tsx +22 -64
  39. package/src/ui/sqlite-row-edit-value.ts +2 -11
  40. package/src/ui/sqlite-row-mutations.ts +10 -32
  41. package/src/ui/sqlite-table-column-order.ts +8 -23
  42. package/src/ui/use-sqlite-requests.ts +46 -86
  43. package/src/ui/utils.ts +1 -5
  44. package/src/ui/value-utils.tsx +3 -11
  45. package/dist/devtools/assets/panel-CIU0JBOs.css +0 -1
  46. package/postcss.config.js +0 -6
package/src/ui/panel.tsx CHANGED
@@ -65,6 +65,14 @@ import {
65
65
  } from './sqlite-introspection';
66
66
  import { QueryResultTable } from './query-result-table';
67
67
  import { SqliteRowDeleteModal } from './sqlite-row-delete-modal';
68
+ import { SqliteDropModal, type SqliteDropModalTarget } from './sqlite-drop-modal';
69
+ import {
70
+ buildDropAllEntitiesSql,
71
+ buildDropEntitySql,
72
+ buildSetForeignKeysSql,
73
+ isForeignKeysEnabled,
74
+ SQLITE_READ_FOREIGN_KEYS_SQL,
75
+ } from './sqlite-drop-mutations';
68
76
  import { SqliteDataTable } from './sqlite-data-table';
69
77
  import { SqliteRowEditModal } from './sqlite-row-edit-modal';
70
78
  import { SqlEditor, type SqlEditorHandle } from './sql-editor';
@@ -100,12 +108,7 @@ import {
100
108
  getDefaultTableColumnOrder,
101
109
  resolveTableColumnOrderUpdate,
102
110
  } from './sqlite-table-column-order';
103
- import {
104
- copyToClipboard,
105
- downloadTextFile,
106
- formatNumber,
107
- slugifyFileName,
108
- } from './utils';
111
+ import { copyToClipboard, downloadTextFile, formatNumber, slugifyFileName } from './utils';
109
112
  import { getResultSummary, getScriptResultSummary } from './value-utils';
110
113
  import './globals.css';
111
114
 
@@ -148,8 +151,16 @@ type ActiveRowMutationState = {
148
151
  rowIndex: number;
149
152
  } | null;
150
153
 
151
- const DEFAULT_QUERY =
152
- 'SELECT name, type FROM sqlite_schema ORDER BY type, name';
154
+ /**
155
+ * The drop dialog operates on the target captured when it opened, so that a
156
+ * mid-dialog explorer reload cannot repoint it at a different object.
157
+ */
158
+ type ActiveDropState = {
159
+ databaseId: string;
160
+ target: SqliteDropModalTarget;
161
+ } | null;
162
+
163
+ const DEFAULT_QUERY = 'SELECT name, type FROM sqlite_schema ORDER BY type, name';
153
164
  const DEFAULT_QUERY_LIMIT = 100;
154
165
  const DEFAULT_PAGE_SIZE = 50;
155
166
  const MIN_EDITOR_HEIGHT = 180;
@@ -164,18 +175,21 @@ const DEFAULT_EXPLORER_STATE: ExplorerState = {
164
175
  loaded: false,
165
176
  };
166
177
 
167
- const joinClassNames = (
168
- ...classNames: Array<string | false | null | undefined>
169
- ) => classNames.filter(Boolean).join(' ');
178
+ const DEFAULT_DROP_TARGET: SqliteDropModalTarget = {
179
+ kind: 'entity',
180
+ entityType: 'table',
181
+ qualifiedName: '',
182
+ confirmationValue: '',
183
+ sql: '',
184
+ };
185
+
186
+ const joinClassNames = (...classNames: Array<string | false | null | undefined>) =>
187
+ classNames.filter(Boolean).join(' ');
170
188
 
171
- const safeError = (error: unknown) =>
172
- error instanceof Error ? error.message : String(error);
189
+ const safeError = (error: unknown) => (error instanceof Error ? error.message : String(error));
173
190
 
174
- const getEntityKey = (
175
- databaseId: string,
176
- schemaName: string,
177
- entityName: string,
178
- ) => JSON.stringify([databaseId, schemaName, entityName]);
191
+ const getEntityKey = (databaseId: string, schemaName: string, entityName: string) =>
192
+ JSON.stringify([databaseId, schemaName, entityName]);
179
193
 
180
194
  const getSchemaKey = (databaseId: string, schemaName: string) =>
181
195
  JSON.stringify([databaseId, schemaName]);
@@ -183,20 +197,12 @@ const getSchemaKey = (databaseId: string, schemaName: string) =>
183
197
  const getLineNumberAtPosition = (value: string, position: number) =>
184
198
  value.slice(0, Math.max(0, position)).split('\n').length;
185
199
 
186
- const buildGeneratedSelect = (
187
- entity: SqliteEntity | null,
188
- rowLimit: number,
189
- ) => {
200
+ const buildGeneratedSelect = (entity: SqliteEntity | null, rowLimit: number) => {
190
201
  if (!entity) {
191
202
  return DEFAULT_QUERY;
192
203
  }
193
204
 
194
- return buildBrowseEntitySql(
195
- entity.schemaName,
196
- entity.name,
197
- Math.max(1, Math.floor(rowLimit)),
198
- 0,
199
- );
205
+ return buildBrowseEntitySql(entity.schemaName, entity.name, Math.max(1, Math.floor(rowLimit)), 0);
200
206
  };
201
207
 
202
208
  const buildCsv = (result: SqliteQueryResult | null) => {
@@ -212,15 +218,11 @@ const buildCsv = (result: SqliteQueryResult | null) => {
212
218
 
213
219
  return [
214
220
  result.columns.join(','),
215
- ...result.rows.map((row) =>
216
- result.columns.map((column) => escapeCell(row[column])).join(','),
217
- ),
221
+ ...result.rows.map((row) => result.columns.map((column) => escapeCell(row[column])).join(',')),
218
222
  ].join('\n');
219
223
  };
220
224
 
221
- const getDefaultSelectedQueryStatementIndex = (
222
- execution: SqliteScriptResult | null,
223
- ) => {
225
+ const getDefaultSelectedQueryStatementIndex = (execution: SqliteScriptResult | null) => {
224
226
  if (!execution || execution.statements.length === 0) {
225
227
  return null;
226
228
  }
@@ -232,18 +234,11 @@ const getDefaultSelectedQueryStatementIndex = (
232
234
  );
233
235
  };
234
236
 
235
- const getStatementQueryResult = (
236
- statement: SqliteScriptStatementResult | null,
237
- ) => statement?.execution?.result ?? null;
237
+ const getStatementQueryResult = (statement: SqliteScriptStatementResult | null) =>
238
+ statement?.execution?.result ?? null;
238
239
 
239
- const getStatementSelectorLabel = (
240
- statement: SqliteScriptStatementResult,
241
- maxLength = 72,
242
- ) => {
243
- const normalizedSql = statement.input.sql
244
- .replace(/\s+/g, ' ')
245
- .replace(/;\s*$/, '')
246
- .trim();
240
+ const getStatementSelectorLabel = (statement: SqliteScriptStatementResult, maxLength = 72) => {
241
+ const normalizedSql = statement.input.sql.replace(/\s+/g, ' ').replace(/;\s*$/, '').trim();
247
242
 
248
243
  if (normalizedSql.length <= maxLength) {
249
244
  return `${formatNumber(statement.index + 1)}. ${normalizedSql}`;
@@ -277,19 +272,13 @@ const buildExplorerGroups = (
277
272
 
278
273
  return schemas
279
274
  .map((schema) => {
280
- const schemaEntities = entities.filter(
281
- (entity) => entity.schemaName === schema.name,
282
- );
275
+ const schemaEntities = entities.filter((entity) => entity.schemaName === schema.name);
283
276
  const filteredEntities = term
284
277
  ? schemaEntities.filter((entity) =>
285
- `${entity.name} ${entity.type} ${schema.name}`
286
- .toLowerCase()
287
- .includes(term),
278
+ `${entity.name} ${entity.type} ${schema.name}`.toLowerCase().includes(term),
288
279
  )
289
280
  : schemaEntities;
290
- const tables = filteredEntities.filter(
291
- (entity) => entity.type === 'table',
292
- );
281
+ const tables = filteredEntities.filter((entity) => entity.type === 'table');
293
282
  const views = filteredEntities.filter((entity) => entity.type === 'view');
294
283
  const visible =
295
284
  term.length === 0 ||
@@ -315,6 +304,7 @@ const iconButtonClassName =
315
304
  'sqlite-icon-button inline-flex h-10 w-10 items-center justify-center rounded-xl';
316
305
  const primaryIconButtonClassName = `${iconButtonClassName} sqlite-button-primary`;
317
306
  const secondaryIconButtonClassName = `${iconButtonClassName} sqlite-button-secondary`;
307
+ const dangerIconButtonClassName = `${iconButtonClassName} sqlite-button-danger`;
318
308
  const ghostIconButtonClassName = `${iconButtonClassName} sqlite-button-ghost`;
319
309
 
320
310
  const renderEmptyState = (
@@ -348,8 +338,7 @@ export default function SqlitePanel() {
348
338
  const client = useRozeniteDevToolsClient<SqliteEventMap>({
349
339
  pluginId: PLUGIN_ID,
350
340
  });
351
- const { requestDatabases, requestQuery, requestScriptExecution } =
352
- useSqliteRequests(client);
341
+ const { requestDatabases, requestQuery, requestScriptExecution } = useSqliteRequests(client);
353
342
 
354
343
  const querySplitRef = useRef<HTMLDivElement | null>(null);
355
344
  const sidebarRef = useRef<HTMLDivElement | null>(null);
@@ -365,19 +354,14 @@ export default function SqlitePanel() {
365
354
  const [editorSplit, setEditorSplit] = useState(50);
366
355
  const [expandedDatabaseIds, setExpandedDatabaseIds] = useState<string[]>([]);
367
356
  const [expandedSchemaKeys, setExpandedSchemaKeys] = useState<string[]>([]);
368
- const [structureSection, setStructureSection] =
369
- useState<StructureSection>('columns');
357
+ const [structureSection, setStructureSection] = useState<StructureSection>('columns');
370
358
 
371
359
  const [databases, setDatabases] = useState<SqliteDatabaseInfo[]>([]);
372
- const [selectedDatabaseId, setSelectedDatabaseId] = useState<string | null>(
373
- null,
374
- );
360
+ const [selectedDatabaseId, setSelectedDatabaseId] = useState<string | null>(null);
375
361
  const [explorerStateByDatabase, setExplorerStateByDatabase] = useState<
376
362
  Record<string, ExplorerState>
377
363
  >({});
378
- const [selectedEntityKey, setSelectedEntityKey] = useState<string | null>(
379
- null,
380
- );
364
+ const [selectedEntityKey, setSelectedEntityKey] = useState<string | null>(null);
381
365
 
382
366
  const [databaseLoading, setDatabaseLoading] = useState(false);
383
367
  const [browseLoading, setBrowseLoading] = useState(false);
@@ -391,9 +375,7 @@ export default function SqlitePanel() {
391
375
 
392
376
  const [browseOffset, setBrowseOffset] = useState(0);
393
377
  const [browsePageSize, setBrowsePageSize] = useState(DEFAULT_PAGE_SIZE);
394
- const [browseResult, setBrowseResult] = useState<SqliteQueryResult | null>(
395
- null,
396
- );
378
+ const [browseResult, setBrowseResult] = useState<SqliteQueryResult | null>(null);
397
379
  const [entityRowCount, setEntityRowCount] = useState<number | null>(null);
398
380
  const [structureState, setStructureState] = useState<StructureState>({
399
381
  columns: [],
@@ -402,37 +384,32 @@ export default function SqlitePanel() {
402
384
  });
403
385
 
404
386
  const [queryInput, setQueryInput] = useState(DEFAULT_QUERY);
405
- const [queryExecution, setQueryExecution] =
406
- useState<SqliteScriptResult | null>(null);
407
- const [selectedQueryStatementIndex, setSelectedQueryStatementIndex] =
408
- useState<number | null>(null);
387
+ const [queryExecution, setQueryExecution] = useState<SqliteScriptResult | null>(null);
388
+ const [selectedQueryStatementIndex, setSelectedQueryStatementIndex] = useState<number | null>(
389
+ null,
390
+ );
409
391
  const [queryRowLimit, setQueryRowLimit] = useState(DEFAULT_QUERY_LIMIT);
410
392
  const [querySelection, setQuerySelection] = useState({ start: 0, end: 0 });
411
393
  const [, setQueryMessage] = useState('Ready.');
412
394
  const [queryErrorLine, setQueryErrorLine] = useState<number | null>(null);
413
- const [queryColumnCache, setQueryColumnCache] = useState(() =>
414
- createSqlEditorColumnCache(),
415
- );
416
- const [tableColumnOrderById, setTableColumnOrderById] = useState<
417
- Record<string, string[]>
418
- >({});
395
+ const [queryColumnCache, setQueryColumnCache] = useState(() => createSqlEditorColumnCache());
396
+ const [tableColumnOrderById, setTableColumnOrderById] = useState<Record<string, string[]>>({});
419
397
  const [editingRow, setEditingRow] = useState<ActiveRowMutationState>(null);
420
398
  const [deletingRow, setDeletingRow] = useState<ActiveRowMutationState>(null);
399
+ const [activeDrop, setActiveDrop] = useState<ActiveDropState>(null);
421
400
 
422
401
  const [objectSearch, setObjectSearch] = useState('');
423
402
  const [dataSearch, setDataSearch] = useState('');
424
403
 
425
404
  const selectedDatabase = useMemo(
426
- () =>
427
- databases.find((database) => database.id === selectedDatabaseId) ?? null,
405
+ () => databases.find((database) => database.id === selectedDatabaseId) ?? null,
428
406
  [databases, selectedDatabaseId],
429
407
  );
430
408
 
431
409
  const selectedExplorerState = useMemo(
432
410
  () =>
433
- (selectedDatabaseId
434
- ? explorerStateByDatabase[selectedDatabaseId]
435
- : null) ?? DEFAULT_EXPLORER_STATE,
411
+ (selectedDatabaseId ? explorerStateByDatabase[selectedDatabaseId] : null) ??
412
+ DEFAULT_EXPLORER_STATE,
436
413
  [explorerStateByDatabase, selectedDatabaseId],
437
414
  );
438
415
 
@@ -445,8 +422,7 @@ export default function SqlitePanel() {
445
422
  entities.find(
446
423
  (entity) =>
447
424
  selectedDatabaseId != null &&
448
- getEntityKey(selectedDatabaseId, entity.schemaName, entity.name) ===
449
- selectedEntityKey,
425
+ getEntityKey(selectedDatabaseId, entity.schemaName, entity.name) === selectedEntityKey,
450
426
  ) ?? null,
451
427
  [entities, selectedDatabaseId, selectedEntityKey],
452
428
  );
@@ -546,20 +522,12 @@ export default function SqlitePanel() {
546
522
  }, [browseResult, filteredBrowseRows]);
547
523
 
548
524
  const dataPageStart = filteredBrowseRows.length > 0 ? browseOffset + 1 : 0;
549
- const dataPageEnd =
550
- filteredBrowseRows.length > 0
551
- ? browseOffset + filteredBrowseRows.length
552
- : 0;
525
+ const dataPageEnd = filteredBrowseRows.length > 0 ? browseOffset + filteredBrowseRows.length : 0;
553
526
  const canBrowseBackward = browseOffset > 0;
554
- const canBrowseForward =
555
- entityRowCount != null && browseOffset + browsePageSize < entityRowCount;
556
- const currentDataPage = selectedEntity
557
- ? Math.floor(browseOffset / browsePageSize) + 1
558
- : 0;
527
+ const canBrowseForward = entityRowCount != null && browseOffset + browsePageSize < entityRowCount;
528
+ const currentDataPage = selectedEntity ? Math.floor(browseOffset / browsePageSize) + 1 : 0;
559
529
  const totalDataPages =
560
- entityRowCount == null || entityRowCount === 0
561
- ? 0
562
- : Math.ceil(entityRowCount / browsePageSize);
530
+ entityRowCount == null || entityRowCount === 0 ? 0 : Math.ceil(entityRowCount / browsePageSize);
563
531
  const primaryKeyColumns = useMemo(
564
532
  () => getPrimaryKeyColumns(structureState.columns),
565
533
  [structureState.columns],
@@ -588,11 +556,8 @@ export default function SqlitePanel() {
588
556
  type: column.type || '—',
589
557
  nullable: column.notNull ? 'No' : 'Yes',
590
558
  defaultValue: column.defaultValue ?? '—',
591
- primaryKey:
592
- column.primaryKeyOrder > 0 ? `PK ${column.primaryKeyOrder}` : '—',
593
- foreignKey: structureState.foreignKeys.some(
594
- (foreignKey) => foreignKey.from === column.name,
595
- )
559
+ primaryKey: column.primaryKeyOrder > 0 ? `PK ${column.primaryKeyOrder}` : '—',
560
+ foreignKey: structureState.foreignKeys.some((foreignKey) => foreignKey.from === column.name)
596
561
  ? 'Yes'
597
562
  : '—',
598
563
  extra: column.hidden > 0 ? `Hidden ${column.hidden}` : '—',
@@ -611,9 +576,7 @@ export default function SqlitePanel() {
611
576
  [structureState.indexes],
612
577
  );
613
578
 
614
- const structureColumnsTableColumns = useMemo<
615
- ColumnDef<StructureColumnRow, unknown>[]
616
- >(
579
+ const structureColumnsTableColumns = useMemo<ColumnDef<StructureColumnRow, unknown>[]>(
617
580
  () => [
618
581
  { id: 'name', header: 'Name', accessorKey: 'name' },
619
582
  { id: 'type', header: 'Type', accessorKey: 'type' },
@@ -626,9 +589,7 @@ export default function SqlitePanel() {
626
589
  [],
627
590
  );
628
591
 
629
- const structureIndexesTableColumns = useMemo<
630
- ColumnDef<StructureIndexRow, unknown>[]
631
- >(
592
+ const structureIndexesTableColumns = useMemo<ColumnDef<StructureIndexRow, unknown>[]>(
632
593
  () => [
633
594
  { id: 'indexName', header: 'Index Name', accessorKey: 'indexName' },
634
595
  { id: 'columns', header: 'Columns', accessorKey: 'columns' },
@@ -638,10 +599,7 @@ export default function SqlitePanel() {
638
599
  [],
639
600
  );
640
601
 
641
- const queryStatements = useMemo(
642
- () => splitSqlStatements(queryInput),
643
- [queryInput],
644
- );
602
+ const queryStatements = useMemo(() => splitSqlStatements(queryInput), [queryInput]);
645
603
 
646
604
  const selectedQueryStatement = useMemo(() => {
647
605
  if (!queryExecution) {
@@ -649,18 +607,13 @@ export default function SqlitePanel() {
649
607
  }
650
608
 
651
609
  const nextIndex =
652
- selectedQueryStatementIndex ??
653
- getDefaultSelectedQueryStatementIndex(queryExecution);
610
+ selectedQueryStatementIndex ?? getDefaultSelectedQueryStatementIndex(queryExecution);
654
611
 
655
612
  if (nextIndex == null) {
656
613
  return null;
657
614
  }
658
615
 
659
- return (
660
- queryExecution.statements.find(
661
- (statement) => statement.index === nextIndex,
662
- ) ?? null
663
- );
616
+ return queryExecution.statements.find((statement) => statement.index === nextIndex) ?? null;
664
617
  }, [queryExecution, selectedQueryStatementIndex]);
665
618
 
666
619
  const activeQueryResult = useMemo(
@@ -671,8 +624,7 @@ export default function SqlitePanel() {
671
624
  const selectedQueryStatementValue = selectedQueryStatement?.index ?? '';
672
625
 
673
626
  const queryTableId = useMemo(
674
- () =>
675
- buildQueryTableId(selectedDatabaseId, activeQueryResult?.columns ?? []),
627
+ () => buildQueryTableId(selectedDatabaseId, activeQueryResult?.columns ?? []),
676
628
  [activeQueryResult?.columns, selectedDatabaseId],
677
629
  );
678
630
 
@@ -710,19 +662,12 @@ export default function SqlitePanel() {
710
662
  );
711
663
 
712
664
  const getTableColumnOrder = useCallback(
713
- (
714
- tableId: string,
715
- columnIds: string[],
716
- fixedLeadingColumnIds: string[] = [],
717
- ) =>
665
+ (tableId: string, columnIds: string[], fixedLeadingColumnIds: string[] = []) =>
718
666
  resolveTableColumnOrderUpdate({
719
667
  columnIds,
720
668
  fixedLeadingColumnIds,
721
669
  storedColumnOrder: tableColumnOrderById[tableId],
722
- nextColumnOrder: getDefaultTableColumnOrder(
723
- columnIds,
724
- fixedLeadingColumnIds,
725
- ),
670
+ nextColumnOrder: getDefaultTableColumnOrder(columnIds, fixedLeadingColumnIds),
726
671
  }),
727
672
  [tableColumnOrderById],
728
673
  );
@@ -777,39 +722,26 @@ export default function SqlitePanel() {
777
722
  );
778
723
 
779
724
  const queryColumnOrder = useMemo(
780
- () =>
781
- getTableColumnOrder(queryTableId, queryColumnIds, [
782
- SQLITE_ROW_NUMBER_COLUMN_ID,
783
- ]),
725
+ () => getTableColumnOrder(queryTableId, queryColumnIds, [SQLITE_ROW_NUMBER_COLUMN_ID]),
784
726
  [getTableColumnOrder, queryColumnIds, queryTableId],
785
727
  );
786
728
  const dataColumnOrder = useMemo(
787
- () =>
788
- getTableColumnOrder(dataTableId, dataColumnIds, [
789
- SQLITE_ROW_NUMBER_COLUMN_ID,
790
- ]),
729
+ () => getTableColumnOrder(dataTableId, dataColumnIds, [SQLITE_ROW_NUMBER_COLUMN_ID]),
791
730
  [dataColumnIds, dataTableId, getTableColumnOrder],
792
731
  );
793
732
  const structureColumnsColumnOrder = useMemo(
794
- () =>
795
- getTableColumnOrder(structureColumnsTableId, structureColumnsColumnIds),
733
+ () => getTableColumnOrder(structureColumnsTableId, structureColumnsColumnIds),
796
734
  [getTableColumnOrder, structureColumnsColumnIds, structureColumnsTableId],
797
735
  );
798
736
  const structureIndexesColumnOrder = useMemo(
799
- () =>
800
- getTableColumnOrder(structureIndexesTableId, structureIndexesColumnIds),
737
+ () => getTableColumnOrder(structureIndexesTableId, structureIndexesColumnIds),
801
738
  [getTableColumnOrder, structureIndexesColumnIds, structureIndexesTableId],
802
739
  );
803
740
 
804
- const setEntitySelection = useCallback(
805
- (databaseId: string, entity: SqliteEntity) => {
806
- setSelectedDatabaseId(databaseId);
807
- setSelectedEntityKey(
808
- getEntityKey(databaseId, entity.schemaName, entity.name),
809
- );
810
- },
811
- [],
812
- );
741
+ const setEntitySelection = useCallback((databaseId: string, entity: SqliteEntity) => {
742
+ setSelectedDatabaseId(databaseId);
743
+ setSelectedEntityKey(getEntityKey(databaseId, entity.schemaName, entity.name));
744
+ }, []);
813
745
 
814
746
  const loadDatabases = useCallback(async () => {
815
747
  setDatabaseLoading(true);
@@ -837,10 +769,7 @@ export default function SqlitePanel() {
837
769
  return [...currentIds, ...missingIds];
838
770
  });
839
771
  setSelectedDatabaseId((current) => {
840
- if (
841
- current &&
842
- nextDatabases.some((database) => database.id === current)
843
- ) {
772
+ if (current && nextDatabases.some((database) => database.id === current)) {
844
773
  return current;
845
774
  }
846
775
 
@@ -899,9 +828,7 @@ export default function SqlitePanel() {
899
828
  },
900
829
  }));
901
830
  setExpandedSchemaKeys((current) => {
902
- const nextKeys = nextSchemas.map((schema) =>
903
- getSchemaKey(databaseId, schema.name),
904
- );
831
+ const nextKeys = nextSchemas.map((schema) => getSchemaKey(databaseId, schema.name));
905
832
  return Array.from(new Set([...current, ...nextKeys]));
906
833
  });
907
834
  } catch (error) {
@@ -948,17 +875,12 @@ export default function SqlitePanel() {
948
875
  selectedEntity.name,
949
876
  browsePageSize,
950
877
  browseOffset,
951
- rowMutationDescriptor?.mode === 'rowid'
952
- ? rowMutationDescriptor.rowIdIdentifier
953
- : null,
878
+ rowMutationDescriptor?.mode === 'rowid' ? rowMutationDescriptor.rowIdIdentifier : null,
954
879
  ),
955
880
  }),
956
881
  requestQuery({
957
882
  databaseId: selectedDatabaseId,
958
- sql: buildEntityCountSql(
959
- selectedEntity.schemaName,
960
- selectedEntity.name,
961
- ),
883
+ sql: buildEntityCountSql(selectedEntity.schemaName, selectedEntity.name),
962
884
  }),
963
885
  ]);
964
886
 
@@ -1021,43 +943,27 @@ export default function SqlitePanel() {
1021
943
  setStructureError(null);
1022
944
 
1023
945
  try {
1024
- const [columnsOutcome, foreignKeysOutcome, indexesOutcome] =
1025
- await Promise.allSettled([
1026
- requestQuery({
1027
- databaseId: selectedDatabaseId,
1028
- sql: buildTableXInfoSql(
1029
- selectedEntity.schemaName,
1030
- selectedEntity.name,
1031
- ),
1032
- }),
1033
- requestQuery({
1034
- databaseId: selectedDatabaseId,
1035
- sql: buildForeignKeySql(
1036
- selectedEntity.schemaName,
1037
- selectedEntity.name,
1038
- ),
1039
- }),
1040
- requestQuery({
1041
- databaseId: selectedDatabaseId,
1042
- sql: buildIndexListSql(
1043
- selectedEntity.schemaName,
1044
- selectedEntity.name,
1045
- ),
1046
- }),
1047
- ]);
946
+ const [columnsOutcome, foreignKeysOutcome, indexesOutcome] = await Promise.allSettled([
947
+ requestQuery({
948
+ databaseId: selectedDatabaseId,
949
+ sql: buildTableXInfoSql(selectedEntity.schemaName, selectedEntity.name),
950
+ }),
951
+ requestQuery({
952
+ databaseId: selectedDatabaseId,
953
+ sql: buildForeignKeySql(selectedEntity.schemaName, selectedEntity.name),
954
+ }),
955
+ requestQuery({
956
+ databaseId: selectedDatabaseId,
957
+ sql: buildIndexListSql(selectedEntity.schemaName, selectedEntity.name),
958
+ }),
959
+ ]);
1048
960
 
1049
961
  const columns =
1050
- columnsOutcome.status === 'fulfilled'
1051
- ? parseColumns(columnsOutcome.value)
1052
- : [];
962
+ columnsOutcome.status === 'fulfilled' ? parseColumns(columnsOutcome.value) : [];
1053
963
  const foreignKeys =
1054
- foreignKeysOutcome.status === 'fulfilled'
1055
- ? parseForeignKeys(foreignKeysOutcome.value)
1056
- : [];
964
+ foreignKeysOutcome.status === 'fulfilled' ? parseForeignKeys(foreignKeysOutcome.value) : [];
1057
965
  const indexes =
1058
- indexesOutcome.status === 'fulfilled'
1059
- ? parseIndexes(indexesOutcome.value)
1060
- : [];
966
+ indexesOutcome.status === 'fulfilled' ? parseIndexes(indexesOutcome.value) : [];
1061
967
 
1062
968
  const enrichedIndexes = await Promise.all(
1063
969
  indexes.map(async (index) => {
@@ -1119,9 +1025,7 @@ export default function SqlitePanel() {
1119
1025
 
1120
1026
  const refreshExplorerData = useCallback(async () => {
1121
1027
  const nextDatabases = await loadDatabases();
1122
- await Promise.all(
1123
- nextDatabases.map((database) => loadExplorer(database.id)),
1124
- );
1028
+ await Promise.all(nextDatabases.map((database) => loadExplorer(database.id)));
1125
1029
  }, [loadDatabases, loadExplorer]);
1126
1030
 
1127
1031
  const refreshWorkspace = useCallback(async () => {
@@ -1134,12 +1038,7 @@ export default function SqlitePanel() {
1134
1038
 
1135
1039
  const handleSaveRow = useCallback(
1136
1040
  async (nextValues: Record<string, unknown>) => {
1137
- if (
1138
- !selectedDatabaseId ||
1139
- !selectedEntity ||
1140
- !editingRow ||
1141
- !rowMutationDescriptor
1142
- ) {
1041
+ if (!selectedDatabaseId || !selectedEntity || !editingRow || !rowMutationDescriptor) {
1143
1042
  throw new Error('The selected row is no longer available.');
1144
1043
  }
1145
1044
 
@@ -1171,12 +1070,7 @@ export default function SqlitePanel() {
1171
1070
  );
1172
1071
 
1173
1072
  const handleDeleteRow = useCallback(async () => {
1174
- if (
1175
- !selectedDatabaseId ||
1176
- !selectedEntity ||
1177
- !deletingRow ||
1178
- !rowMutationDescriptor
1179
- ) {
1073
+ if (!selectedDatabaseId || !selectedEntity || !deletingRow || !rowMutationDescriptor) {
1180
1074
  throw new Error('The selected row is no longer available.');
1181
1075
  }
1182
1076
 
@@ -1202,9 +1096,140 @@ export default function SqlitePanel() {
1202
1096
  selectedEntity,
1203
1097
  ]);
1204
1098
 
1099
+ const handleOpenDropEntity = useCallback(() => {
1100
+ if (!selectedDatabaseId || !selectedEntity) {
1101
+ return;
1102
+ }
1103
+
1104
+ setActiveDrop({
1105
+ databaseId: selectedDatabaseId,
1106
+ target: {
1107
+ kind: 'entity',
1108
+ entityType: selectedEntity.type,
1109
+ qualifiedName: `${selectedEntity.schemaName}.${selectedEntity.name}`,
1110
+ confirmationValue: selectedEntity.name,
1111
+ sql: `${buildDropEntitySql(selectedEntity)};`,
1112
+ },
1113
+ });
1114
+ }, [selectedDatabaseId, selectedEntity]);
1115
+
1116
+ const handleOpenDropAllEntities = useCallback(() => {
1117
+ if (!selectedDatabaseId || !selectedDatabase) {
1118
+ return;
1119
+ }
1120
+
1121
+ const tableCount = entities.filter((entity) => entity.type === 'table').length;
1122
+ const viewCount = entities.filter((entity) => entity.type === 'view').length;
1123
+
1124
+ setActiveDrop({
1125
+ databaseId: selectedDatabaseId,
1126
+ target: {
1127
+ kind: 'database',
1128
+ databaseName: selectedDatabase.name,
1129
+ confirmationValue: selectedDatabase.name,
1130
+ tableCount,
1131
+ viewCount,
1132
+ sql: buildDropAllEntitiesSql(entities),
1133
+ },
1134
+ });
1135
+ }, [entities, selectedDatabase, selectedDatabaseId]);
1136
+
1137
+ const dropSelectedEntity = useCallback(
1138
+ async (databaseId: string, sql: string) => {
1139
+ await requestQuery({ databaseId, sql });
1140
+ },
1141
+ [requestQuery],
1142
+ );
1143
+
1144
+ const dropAllEntities = useCallback(
1145
+ async (databaseId: string, sql: string) => {
1146
+ // Dropping a parent table before its children fails while foreign key
1147
+ // enforcement is on, so turn it off for the sweep. The pragma is
1148
+ // per-connection and that connection belongs to the running app, so the
1149
+ // original value has to be restored on every path out of here.
1150
+ const foreignKeysResult = await requestQuery({
1151
+ databaseId,
1152
+ sql: SQLITE_READ_FOREIGN_KEYS_SQL,
1153
+ });
1154
+ const foreignKeysWereEnabled = isForeignKeysEnabled(foreignKeysResult.rows);
1155
+
1156
+ if (foreignKeysWereEnabled) {
1157
+ await requestQuery({
1158
+ databaseId,
1159
+ sql: buildSetForeignKeysSql(false),
1160
+ });
1161
+ }
1162
+
1163
+ let dropError: unknown = null;
1164
+
1165
+ try {
1166
+ const scriptResult = await requestScriptExecution({
1167
+ databaseId,
1168
+ sql,
1169
+ });
1170
+
1171
+ if (scriptResult.failedStatementIndex != null) {
1172
+ const failedStatement = scriptResult.statements.find(
1173
+ (statement) => statement.index === scriptResult.failedStatementIndex,
1174
+ );
1175
+ throw new Error(failedStatement?.error ?? 'Script execution failed.');
1176
+ }
1177
+ } catch (error) {
1178
+ dropError = error;
1179
+ }
1180
+
1181
+ if (foreignKeysWereEnabled) {
1182
+ try {
1183
+ await requestQuery({
1184
+ databaseId,
1185
+ sql: buildSetForeignKeysSql(true),
1186
+ });
1187
+ } catch (restoreError) {
1188
+ // The drop error, if any, takes priority: it's the reason the user
1189
+ // is looking at this modal. Only surface the restore failure when
1190
+ // the drop itself succeeded, since that's a new, silent problem.
1191
+ if (!dropError) {
1192
+ throw restoreError instanceof Error ? restoreError : new Error(String(restoreError));
1193
+ }
1194
+ }
1195
+ }
1196
+
1197
+ if (dropError) {
1198
+ throw dropError instanceof Error ? dropError : new Error(String(dropError));
1199
+ }
1200
+ },
1201
+ [requestQuery, requestScriptExecution],
1202
+ );
1203
+
1204
+ const handleConfirmDrop = useCallback(async () => {
1205
+ if (!activeDrop) {
1206
+ throw new Error('The drop target is no longer available.');
1207
+ }
1208
+
1209
+ // Execute the target captured when the dialog opened rather than the
1210
+ // current selection: an app reload can fire `sqlite:ready` mid-dialog,
1211
+ // which reloads the explorer and may move the selection elsewhere. The
1212
+ // user confirmed by typing this object's name, so this is the object that
1213
+ // has to be dropped.
1214
+ const { databaseId, target } = activeDrop;
1215
+
1216
+ if (!target.sql) {
1217
+ throw new Error('There is nothing to drop.');
1218
+ }
1219
+
1220
+ if (target.kind === 'entity') {
1221
+ await dropSelectedEntity(databaseId, target.sql);
1222
+ } else {
1223
+ await dropAllEntities(databaseId, target.sql);
1224
+ }
1225
+
1226
+ setActiveDrop(null);
1227
+ setSelectedEntityKey(null);
1228
+ await refreshExplorerData();
1229
+ }, [activeDrop, dropAllEntities, dropSelectedEntity, refreshExplorerData]);
1230
+
1205
1231
  const getActiveStatement = useCallback(() => {
1206
- const cursorPosition =
1207
- editorRef.current?.getSelection().start ?? querySelection.start;
1232
+ const cursorPosition = editorRef.current?.getSelection().start ?? querySelection.start;
1208
1233
  const currentStatement = getStatementAtCursor(queryInput, cursorPosition);
1209
1234
  const start = currentStatement?.start ?? 0;
1210
1235
  const end = currentStatement?.end ?? queryInput.length;
@@ -1313,25 +1338,18 @@ export default function SqlitePanel() {
1313
1338
  execution.failedStatementIndex == null
1314
1339
  ? null
1315
1340
  : (execution.statements.find(
1316
- (statement) =>
1317
- statement.index === execution.failedStatementIndex,
1341
+ (statement) => statement.index === execution.failedStatementIndex,
1318
1342
  ) ?? null);
1319
1343
 
1320
1344
  setQueryExecution(execution);
1321
- setSelectedQueryStatementIndex(
1322
- getDefaultSelectedQueryStatementIndex(execution),
1323
- );
1345
+ setSelectedQueryStatementIndex(getDefaultSelectedQueryStatementIndex(execution));
1324
1346
 
1325
1347
  if (failedStatement?.error) {
1326
1348
  setQueryError(failedStatement.error);
1327
- setQueryErrorLine(
1328
- getLineNumberAtPosition(queryInput, failedStatement.start),
1329
- );
1349
+ setQueryErrorLine(getLineNumberAtPosition(queryInput, failedStatement.start));
1330
1350
  }
1331
1351
 
1332
- setQueryMessage(
1333
- getScriptResultSummary(execution) ?? 'Script execution completed.',
1334
- );
1352
+ setQueryMessage(getScriptResultSummary(execution) ?? 'Script execution completed.');
1335
1353
 
1336
1354
  if (hasMutatingStatements(execution)) {
1337
1355
  await refreshWorkspace();
@@ -1340,9 +1358,7 @@ export default function SqlitePanel() {
1340
1358
  setQueryExecution(null);
1341
1359
  setSelectedQueryStatementIndex(null);
1342
1360
  setQueryError(safeError(error));
1343
- setQueryErrorLine(
1344
- getLineNumberAtPosition(queryInput, querySelection.start),
1345
- );
1361
+ setQueryErrorLine(getLineNumberAtPosition(queryInput, querySelection.start));
1346
1362
  setQueryMessage('Execution failed.');
1347
1363
  } finally {
1348
1364
  setQueryLoading(false);
@@ -1364,22 +1380,12 @@ export default function SqlitePanel() {
1364
1380
 
1365
1381
  const handleRunCurrentStatement = useCallback(async () => {
1366
1382
  try {
1367
- await runSingleStatement(
1368
- getActiveStatement(),
1369
- 'Running current statement',
1370
- );
1383
+ await runSingleStatement(getActiveStatement(), 'Running current statement');
1371
1384
  } catch (error) {
1372
1385
  setQueryError(safeError(error));
1373
- setQueryErrorLine(
1374
- getLineNumberAtPosition(queryInput, querySelection.start),
1375
- );
1386
+ setQueryErrorLine(getLineNumberAtPosition(queryInput, querySelection.start));
1376
1387
  }
1377
- }, [
1378
- getActiveStatement,
1379
- queryInput,
1380
- querySelection.start,
1381
- runSingleStatement,
1382
- ]);
1388
+ }, [getActiveStatement, queryInput, querySelection.start, runSingleStatement]);
1383
1389
 
1384
1390
  const handleSaveQuery = useCallback(() => {
1385
1391
  const fileName = `${slugifyFileName(selectedEntity?.name ?? 'query')}.sql`;
@@ -1422,9 +1428,7 @@ export default function SqlitePanel() {
1422
1428
  setQueryInput(formatted);
1423
1429
  setQueryError(null);
1424
1430
  setQueryErrorLine(null);
1425
- setQueryMessage(
1426
- formatted ? 'Formatted query.' : 'Cleared query formatting.',
1427
- );
1431
+ setQueryMessage(formatted ? 'Formatted query.' : 'Cleared query formatting.');
1428
1432
  } catch (error) {
1429
1433
  setQueryError(safeError(error));
1430
1434
  setQueryErrorLine(null);
@@ -1455,13 +1459,7 @@ export default function SqlitePanel() {
1455
1459
  const columns = parseColumns(result);
1456
1460
 
1457
1461
  setQueryColumnCache((current) =>
1458
- setSqlEditorCachedColumns(
1459
- current,
1460
- selectedDatabaseId,
1461
- schemaName,
1462
- entityName,
1463
- columns,
1464
- ),
1462
+ setSqlEditorCachedColumns(current, selectedDatabaseId, schemaName, entityName, columns),
1465
1463
  );
1466
1464
 
1467
1465
  return columns;
@@ -1479,24 +1477,18 @@ export default function SqlitePanel() {
1479
1477
  return null;
1480
1478
  }
1481
1479
 
1482
- const aliases = extractSqlEditorAliases(
1483
- context.state.doc.sliceString(0, context.pos),
1484
- );
1480
+ const aliases = extractSqlEditorAliases(context.state.doc.sliceString(0, context.pos));
1485
1481
  const entity = resolveSqlEditorEntityReference({
1486
1482
  aliases,
1487
1483
  entities,
1488
1484
  request,
1489
- selectedSchemaName:
1490
- selectedEntity?.schemaName ?? defaultCompletionSchemaName ?? null,
1485
+ selectedSchemaName: selectedEntity?.schemaName ?? defaultCompletionSchemaName ?? null,
1491
1486
  });
1492
1487
  if (!entity) {
1493
1488
  return null;
1494
1489
  }
1495
1490
 
1496
- const columns = await ensureQueryEntityColumns(
1497
- entity.schemaName,
1498
- entity.name,
1499
- );
1491
+ const columns = await ensureQueryEntityColumns(entity.schemaName, entity.name);
1500
1492
  if (context.aborted || columns.length === 0) {
1501
1493
  return null;
1502
1494
  }
@@ -1508,12 +1500,7 @@ export default function SqlitePanel() {
1508
1500
  validFor: /^[A-Za-z_][\w$]*$/,
1509
1501
  };
1510
1502
  },
1511
- [
1512
- defaultCompletionSchemaName,
1513
- ensureQueryEntityColumns,
1514
- entities,
1515
- selectedEntity?.schemaName,
1516
- ],
1503
+ [defaultCompletionSchemaName, ensureQueryEntityColumns, entities, selectedEntity?.schemaName],
1517
1504
  );
1518
1505
 
1519
1506
  const handleSidebarResizeStart = useCallback(
@@ -1604,11 +1591,7 @@ export default function SqlitePanel() {
1604
1591
  }, [refreshExplorerData]);
1605
1592
 
1606
1593
  useEffect(() => {
1607
- if (
1608
- !selectedDatabaseId ||
1609
- selectedExplorerState.loading ||
1610
- !selectedExplorerState.loaded
1611
- ) {
1594
+ if (!selectedDatabaseId || selectedExplorerState.loading || !selectedExplorerState.loaded) {
1612
1595
  return;
1613
1596
  }
1614
1597
 
@@ -1616,9 +1599,7 @@ export default function SqlitePanel() {
1616
1599
  if (
1617
1600
  current &&
1618
1601
  selectedExplorerState.entities.some(
1619
- (entity) =>
1620
- getEntityKey(selectedDatabaseId, entity.schemaName, entity.name) ===
1621
- current,
1602
+ (entity) => getEntityKey(selectedDatabaseId, entity.schemaName, entity.name) === current,
1622
1603
  )
1623
1604
  ) {
1624
1605
  return current;
@@ -1626,11 +1607,7 @@ export default function SqlitePanel() {
1626
1607
 
1627
1608
  const fallbackEntity = selectedExplorerState.entities[0];
1628
1609
  return fallbackEntity
1629
- ? getEntityKey(
1630
- selectedDatabaseId,
1631
- fallbackEntity.schemaName,
1632
- fallbackEntity.name,
1633
- )
1610
+ ? getEntityKey(selectedDatabaseId, fallbackEntity.schemaName, fallbackEntity.name)
1634
1611
  : null;
1635
1612
  });
1636
1613
  }, [
@@ -1670,18 +1647,11 @@ export default function SqlitePanel() {
1670
1647
  }, [loadBrowse, selectedEntityKey]);
1671
1648
 
1672
1649
  useEffect(() => {
1673
- setQueryColumnCache((current) =>
1674
- syncSqlEditorColumnCacheDatabase(current, selectedDatabaseId),
1675
- );
1650
+ setQueryColumnCache((current) => syncSqlEditorColumnCacheDatabase(current, selectedDatabaseId));
1676
1651
  }, [selectedDatabaseId]);
1677
1652
 
1678
1653
  useEffect(() => {
1679
- if (
1680
- !selectedDatabaseId ||
1681
- !selectedEntity ||
1682
- structureLoading ||
1683
- structureError
1684
- ) {
1654
+ if (!selectedDatabaseId || !selectedEntity || structureLoading || structureError) {
1685
1655
  return;
1686
1656
  }
1687
1657
 
@@ -1826,10 +1796,7 @@ export default function SqlitePanel() {
1826
1796
  )
1827
1797
  ) : (
1828
1798
  <div ref={querySplitRef} className="sqlite-query-layout">
1829
- <section
1830
- className="sqlite-query-editor-pane"
1831
- style={{ flex: `0 0 ${editorSplit}%` }}
1832
- >
1799
+ <section className="sqlite-query-editor-pane" style={{ flex: `0 0 ${editorSplit}%` }}>
1833
1800
  {queryTabHeader}
1834
1801
  <div className="sqlite-editor-frame">
1835
1802
  <SqlEditor
@@ -1837,14 +1804,8 @@ export default function SqlitePanel() {
1837
1804
  ariaLabel="SQL query editor"
1838
1805
  completionSchema={editorCompletionSchema}
1839
1806
  completionSource={editorCompletionSource}
1840
- defaultSchema={
1841
- selectedEntity?.schemaName ?? defaultCompletionSchemaName
1842
- }
1843
- defaultTable={
1844
- cachedSelectedEntityColumns.length > 0
1845
- ? selectedEntity?.name
1846
- : undefined
1847
- }
1807
+ defaultSchema={selectedEntity?.schemaName ?? defaultCompletionSchemaName}
1808
+ defaultTable={cachedSelectedEntityColumns.length > 0 ? selectedEntity?.name : undefined}
1848
1809
  errorLine={queryErrorLine}
1849
1810
  onFormat={handleFormatQuery}
1850
1811
  onRun={() => void handleRun()}
@@ -1872,9 +1833,7 @@ export default function SqlitePanel() {
1872
1833
  <div className="sqlite-results-header sqlite-query-results-header">
1873
1834
  <div className="sqlite-toolbar-actions sqlite-query-results-header-main">
1874
1835
  {!queryExecution ? (
1875
- <span className="sqlite-helper-text">
1876
- Run SQL to inspect per-statement results.
1877
- </span>
1836
+ <span className="sqlite-helper-text">Run SQL to inspect per-statement results.</span>
1878
1837
  ) : null}
1879
1838
 
1880
1839
  {queryExecution && queryExecution.statements.length > 1 ? (
@@ -1950,8 +1909,7 @@ export default function SqlitePanel() {
1950
1909
  <div className="sqlite-inline-error" aria-live="polite">
1951
1910
  <div>
1952
1911
  <p className="font-medium text-rose-100">
1953
- {(queryExecution?.totalStatementCount ??
1954
- queryStatements.length) > 1
1912
+ {(queryExecution?.totalStatementCount ?? queryStatements.length) > 1
1955
1913
  ? 'Script Error'
1956
1914
  : 'SQL Error'}
1957
1915
  </p>
@@ -1962,11 +1920,7 @@ export default function SqlitePanel() {
1962
1920
  </p>
1963
1921
  ) : null}
1964
1922
  </div>
1965
- <button
1966
- type="button"
1967
- className={ghostButtonClassName}
1968
- onClick={handleCopyError}
1969
- >
1923
+ <button type="button" className={ghostButtonClassName} onClick={handleCopyError}>
1970
1924
  <Copy aria-hidden="true" className="h-4 w-4" />
1971
1925
  Copy Error
1972
1926
  </button>
@@ -1979,20 +1933,15 @@ export default function SqlitePanel() {
1979
1933
  result={activeQueryResult}
1980
1934
  columnOrder={queryColumnOrder}
1981
1935
  onColumnOrderChange={(nextColumnOrder) =>
1982
- setTableColumnOrder(
1983
- queryTableId,
1984
- queryColumnIds,
1985
- nextColumnOrder,
1986
- [SQLITE_ROW_NUMBER_COLUMN_ID],
1987
- )
1936
+ setTableColumnOrder(queryTableId, queryColumnIds, nextColumnOrder, [
1937
+ SQLITE_ROW_NUMBER_COLUMN_ID,
1938
+ ])
1988
1939
  }
1989
1940
  loading={queryLoading}
1990
1941
  showMetadata={false}
1991
1942
  shellClassName="h-full min-h-0"
1992
1943
  scrollContainerClassName="min-h-0 sqlite-results-scroll-flush"
1993
- emptyTitle={
1994
- selectedQueryStatement?.error ? 'Statement Failed' : 'No Results'
1995
- }
1944
+ emptyTitle={selectedQueryStatement?.error ? 'Statement Failed' : 'No Results'}
1996
1945
  emptyDescription={
1997
1946
  selectedQueryStatement?.error
1998
1947
  ? 'Select another statement to inspect its rows, or fix the error and run again.'
@@ -2019,9 +1968,7 @@ export default function SqlitePanel() {
2019
1968
  disabled={editableColumns.length === 0}
2020
1969
  aria-label={`Edit row ${rowNumber}`}
2021
1970
  title={
2022
- editableColumns.length === 0
2023
- ? 'No editable columns'
2024
- : `Edit row ${rowNumber}`
1971
+ editableColumns.length === 0 ? 'No editable columns' : `Edit row ${rowNumber}`
2025
1972
  }
2026
1973
  onClick={(event) => {
2027
1974
  event.stopPropagation();
@@ -2063,11 +2010,7 @@ export default function SqlitePanel() {
2063
2010
  'database',
2064
2011
  )
2065
2012
  ) : !selectedEntity ? (
2066
- renderEmptyState(
2067
- 'Select A Table',
2068
- 'Choose a table in the sidebar to view its rows.',
2069
- 'table',
2070
- )
2013
+ renderEmptyState('Select A Table', 'Choose a table in the sidebar to view its rows.', 'table')
2071
2014
  ) : (
2072
2015
  <div className="sqlite-content-stack">
2073
2016
  <header className="sqlite-object-header">
@@ -2092,11 +2035,7 @@ export default function SqlitePanel() {
2092
2035
  </div>
2093
2036
  </div>
2094
2037
  {dataSearch.trim() ? (
2095
- <button
2096
- type="button"
2097
- className="sqlite-chip"
2098
- onClick={() => setDataSearch('')}
2099
- >
2038
+ <button type="button" className="sqlite-chip" onClick={() => setDataSearch('')}>
2100
2039
  contains {dataSearch}
2101
2040
  <X aria-hidden="true" className="h-3.5 w-3.5" />
2102
2041
  </button>
@@ -2114,10 +2053,7 @@ export default function SqlitePanel() {
2114
2053
  >
2115
2054
  <RefreshCw
2116
2055
  aria-hidden="true"
2117
- className={joinClassNames(
2118
- 'h-4 w-4',
2119
- browseLoading && 'animate-spin',
2120
- )}
2056
+ className={joinClassNames('h-4 w-4', browseLoading && 'animate-spin')}
2121
2057
  />
2122
2058
  </button>
2123
2059
  </div>
@@ -2146,9 +2082,7 @@ export default function SqlitePanel() {
2146
2082
  showMetadata={false}
2147
2083
  shellClassName="h-full min-h-0"
2148
2084
  scrollContainerClassName="min-h-0 sqlite-results-scroll-flush"
2149
- emptyTitle={
2150
- selectedEntity ? 'No Rows On This Page' : 'No Table Selected'
2151
- }
2085
+ emptyTitle={selectedEntity ? 'No Rows On This Page' : 'No Table Selected'}
2152
2086
  emptyDescription={
2153
2087
  selectedEntity
2154
2088
  ? 'This page does not contain rows.'
@@ -2198,11 +2132,7 @@ export default function SqlitePanel() {
2198
2132
  <button
2199
2133
  type="button"
2200
2134
  className={secondaryButtonClassName}
2201
- onClick={() =>
2202
- setBrowseOffset((current) =>
2203
- Math.max(0, current - browsePageSize),
2204
- )
2205
- }
2135
+ onClick={() => setBrowseOffset((current) => Math.max(0, current - browsePageSize))}
2206
2136
  disabled={browseLoading || !canBrowseBackward}
2207
2137
  >
2208
2138
  Previous
@@ -2210,17 +2140,13 @@ export default function SqlitePanel() {
2210
2140
  <button
2211
2141
  type="button"
2212
2142
  className={secondaryButtonClassName}
2213
- onClick={() =>
2214
- setBrowseOffset((current) => current + browsePageSize)
2215
- }
2143
+ onClick={() => setBrowseOffset((current) => current + browsePageSize)}
2216
2144
  disabled={browseLoading || !canBrowseForward}
2217
2145
  >
2218
2146
  Next
2219
2147
  </button>
2220
2148
  <span className="sqlite-badge sqlite-badge-neutral sqlite-tabular">
2221
- {totalDataPages > 0
2222
- ? `${currentDataPage}/${totalDataPages}`
2223
- : '0/0'}
2149
+ {totalDataPages > 0 ? `${currentDataPage}/${totalDataPages}` : '0/0'}
2224
2150
  </span>
2225
2151
  </div>
2226
2152
  </footer>
@@ -2242,11 +2168,7 @@ export default function SqlitePanel() {
2242
2168
  ) : (
2243
2169
  <div className="sqlite-content-stack">
2244
2170
  <header className="sqlite-object-header">
2245
- <div
2246
- className="sqlite-section-tabs"
2247
- role="tablist"
2248
- aria-label="Structure sections"
2249
- >
2171
+ <div className="sqlite-section-tabs" role="tablist" aria-label="Structure sections">
2250
2172
  {(
2251
2173
  [
2252
2174
  ['columns', 'Columns'],
@@ -2281,12 +2203,18 @@ export default function SqlitePanel() {
2281
2203
  >
2282
2204
  <RefreshCw
2283
2205
  aria-hidden="true"
2284
- className={joinClassNames(
2285
- 'h-4 w-4',
2286
- structureLoading && 'animate-spin',
2287
- )}
2206
+ className={joinClassNames('h-4 w-4', structureLoading && 'animate-spin')}
2288
2207
  />
2289
2208
  </button>
2209
+ <button
2210
+ type="button"
2211
+ className={dangerIconButtonClassName}
2212
+ onClick={handleOpenDropEntity}
2213
+ aria-label={selectedEntity.type === 'view' ? 'Drop view' : 'Drop table'}
2214
+ title={selectedEntity.type === 'view' ? 'Drop view' : 'Drop table'}
2215
+ >
2216
+ <Trash2 aria-hidden="true" className="h-4 w-4" />
2217
+ </button>
2290
2218
  </div>
2291
2219
  </header>
2292
2220
 
@@ -2344,19 +2272,10 @@ export default function SqlitePanel() {
2344
2272
  ) : (
2345
2273
  <div className="sqlite-chip-row">
2346
2274
  {primaryKeyColumns
2347
- .sort(
2348
- (left, right) =>
2349
- left.primaryKeyOrder - right.primaryKeyOrder,
2350
- )
2275
+ .sort((left, right) => left.primaryKeyOrder - right.primaryKeyOrder)
2351
2276
  .map((column) => (
2352
- <span
2353
- key={column.name}
2354
- className="sqlite-chip sqlite-chip-static"
2355
- >
2356
- <KeyRound
2357
- aria-hidden="true"
2358
- className="h-3.5 w-3.5"
2359
- />
2277
+ <span key={column.name} className="sqlite-chip sqlite-chip-static">
2278
+ <KeyRound aria-hidden="true" className="h-3.5 w-3.5" />
2360
2279
  {column.name}
2361
2280
  </span>
2362
2281
  ))}
@@ -2373,18 +2292,14 @@ export default function SqlitePanel() {
2373
2292
  ) : (
2374
2293
  <div className="space-y-3">
2375
2294
  {structureState.foreignKeys.map((foreignKey) => (
2376
- <div
2377
- key={`${foreignKey.id}-${foreignKey.seq}`}
2378
- className="sqlite-key-row"
2379
- >
2295
+ <div key={`${foreignKey.id}-${foreignKey.seq}`} className="sqlite-key-row">
2380
2296
  <div>
2381
2297
  <p className="font-medium text-white">
2382
2298
  {foreignKey.from} → {foreignKey.table}
2383
2299
  {foreignKey.to ? `.${foreignKey.to}` : ''}
2384
2300
  </p>
2385
2301
  <p className="sqlite-helper-text">
2386
- Update {foreignKey.onUpdate} · Delete{' '}
2387
- {foreignKey.onDelete}
2302
+ Update {foreignKey.onUpdate} · Delete {foreignKey.onDelete}
2388
2303
  </p>
2389
2304
  </div>
2390
2305
  <span className="sqlite-badge sqlite-badge-neutral">
@@ -2428,11 +2343,7 @@ export default function SqlitePanel() {
2428
2343
  Skip To Workspace
2429
2344
  </a>
2430
2345
  <div className="sqlite-app-body">
2431
- <aside
2432
- ref={sidebarRef}
2433
- className="sqlite-sidebar-wrap"
2434
- style={{ width: sidebarWidth }}
2435
- >
2346
+ <aside ref={sidebarRef} className="sqlite-sidebar-wrap" style={{ width: sidebarWidth }}>
2436
2347
  <section className="sqlite-sidebar-panel">
2437
2348
  <header className="sqlite-sidebar-header">
2438
2349
  <div className="sqlite-toolbar-actions">
@@ -2455,6 +2366,26 @@ export default function SqlitePanel() {
2455
2366
  )}
2456
2367
  />
2457
2368
  </button>
2369
+ <button
2370
+ type="button"
2371
+ className={dangerIconButtonClassName}
2372
+ aria-label={
2373
+ selectedDatabase
2374
+ ? `Drop all objects in ${selectedDatabase.name}`
2375
+ : 'Drop all objects'
2376
+ }
2377
+ title={
2378
+ selectedDatabase
2379
+ ? `Drop all objects in ${selectedDatabase.name}`
2380
+ : 'Drop all objects'
2381
+ }
2382
+ onClick={handleOpenDropAllEntities}
2383
+ disabled={
2384
+ !selectedDatabaseId || entities.length === 0 || databaseLoading || entityLoading
2385
+ }
2386
+ >
2387
+ <Trash2 aria-hidden="true" className="h-4 w-4" />
2388
+ </button>
2458
2389
  </div>
2459
2390
  </header>
2460
2391
 
@@ -2487,9 +2418,7 @@ export default function SqlitePanel() {
2487
2418
  </div>
2488
2419
  ) : databases.length === 0 ? (
2489
2420
  renderEmptyState(
2490
- databaseError
2491
- ? 'Could Not Load Databases'
2492
- : 'No Databases Found',
2421
+ databaseError ? 'Could Not Load Databases' : 'No Databases Found',
2493
2422
  databaseError ??
2494
2423
  'Expose a SQLite adapter in your app, then refresh to inspect it here.',
2495
2424
  'database',
@@ -2497,12 +2426,9 @@ export default function SqlitePanel() {
2497
2426
  ) : (
2498
2427
  <div className="sqlite-connection-list">
2499
2428
  {databases.map((database) => {
2500
- const isExpanded = expandedDatabaseIds.includes(
2501
- database.id,
2502
- );
2429
+ const isExpanded = expandedDatabaseIds.includes(database.id);
2503
2430
  const databaseExplorerState =
2504
- explorerStateByDatabase[database.id] ??
2505
- DEFAULT_EXPLORER_STATE;
2431
+ explorerStateByDatabase[database.id] ?? DEFAULT_EXPLORER_STATE;
2506
2432
  const databaseExplorerGroups = buildExplorerGroups(
2507
2433
  databaseExplorerState.schemas,
2508
2434
  databaseExplorerState.entities,
@@ -2528,206 +2454,147 @@ export default function SqlitePanel() {
2528
2454
  }}
2529
2455
  >
2530
2456
  {isExpanded ? (
2531
- <ChevronDown
2532
- aria-hidden="true"
2533
- className="h-4 w-4 shrink-0"
2534
- />
2457
+ <ChevronDown aria-hidden="true" className="h-4 w-4 shrink-0" />
2535
2458
  ) : (
2536
- <ChevronRight
2537
- aria-hidden="true"
2538
- className="h-4 w-4 shrink-0"
2539
- />
2459
+ <ChevronRight aria-hidden="true" className="h-4 w-4 shrink-0" />
2540
2460
  )}
2541
- <Database
2542
- aria-hidden="true"
2543
- className="h-4 w-4 shrink-0"
2544
- />
2545
- <span className="min-w-0 truncate font-medium">
2546
- {database.name}
2547
- </span>
2461
+ <Database aria-hidden="true" className="h-4 w-4 shrink-0" />
2462
+ <span className="min-w-0 truncate font-medium">{database.name}</span>
2548
2463
  </button>
2549
2464
 
2550
2465
  {isExpanded ? (
2551
2466
  <div className="sqlite-tree-shell">
2552
- {databaseExplorerState.loading ||
2553
- !databaseExplorerState.loaded ? (
2554
- <div
2555
- className="sqlite-sidebar-skeleton"
2556
- aria-live="polite"
2557
- >
2467
+ {databaseExplorerState.loading || !databaseExplorerState.loaded ? (
2468
+ <div className="sqlite-sidebar-skeleton" aria-live="polite">
2558
2469
  {Array.from({ length: 4 }, (_, index) => (
2559
- <div
2560
- key={index}
2561
- className="sqlite-sidebar-skeleton-row"
2562
- />
2470
+ <div key={index} className="sqlite-sidebar-skeleton-row" />
2563
2471
  ))}
2564
2472
  </div>
2565
2473
  ) : databaseExplorerState.error ? (
2566
- <div
2567
- className="sqlite-inline-error"
2568
- aria-live="polite"
2569
- >
2474
+ <div className="sqlite-inline-error" aria-live="polite">
2570
2475
  <div>
2571
- <p className="font-medium text-rose-100">
2572
- Explorer Load Failed
2573
- </p>
2476
+ <p className="font-medium text-rose-100">Explorer Load Failed</p>
2574
2477
  <p className="mt-1 text-sm text-rose-100/90">
2575
2478
  {databaseExplorerState.error}
2576
2479
  </p>
2577
2480
  </div>
2578
2481
  </div>
2579
2482
  ) : databaseExplorerGroups.length === 0 ? (
2580
- <div className="sqlite-tree-empty">
2581
- No objects match this filter.
2582
- </div>
2483
+ <div className="sqlite-tree-empty">No objects match this filter.</div>
2583
2484
  ) : (
2584
- databaseExplorerGroups.map(
2585
- ({ schema, tables, views }) => {
2586
- const schemaKey = getSchemaKey(
2587
- database.id,
2588
- schema.name,
2589
- );
2590
- const isSchemaExpanded =
2591
- expandedSchemaKeys.includes(schemaKey);
2592
-
2593
- return (
2594
- <div
2595
- key={`${database.id}-${schema.name}`}
2596
- className="sqlite-schema-group"
2597
- >
2598
- <button
2599
- type="button"
2600
- className="sqlite-schema-row"
2601
- onClick={() => {
2602
- setExpandedSchemaKeys((current) =>
2603
- current.includes(schemaKey)
2604
- ? current.filter(
2605
- (value) =>
2606
- value !== schemaKey,
2607
- )
2608
- : [...current, schemaKey],
2609
- );
2610
- }}
2611
- >
2612
- {isSchemaExpanded ? (
2613
- <ChevronDown
2614
- aria-hidden="true"
2615
- className="h-4 w-4"
2616
- />
2617
- ) : (
2618
- <ChevronRight
2619
- aria-hidden="true"
2620
- className="h-4 w-4"
2621
- />
2622
- )}
2623
- <FolderTree
2624
- aria-hidden="true"
2625
- className="h-4 w-4"
2626
- />
2627
- <span className="min-w-0 flex-1 truncate">
2628
- {schema.name}
2629
- </span>
2630
- </button>
2485
+ databaseExplorerGroups.map(({ schema, tables, views }) => {
2486
+ const schemaKey = getSchemaKey(database.id, schema.name);
2487
+ const isSchemaExpanded = expandedSchemaKeys.includes(schemaKey);
2631
2488
 
2489
+ return (
2490
+ <div
2491
+ key={`${database.id}-${schema.name}`}
2492
+ className="sqlite-schema-group"
2493
+ >
2494
+ <button
2495
+ type="button"
2496
+ className="sqlite-schema-row"
2497
+ onClick={() => {
2498
+ setExpandedSchemaKeys((current) =>
2499
+ current.includes(schemaKey)
2500
+ ? current.filter((value) => value !== schemaKey)
2501
+ : [...current, schemaKey],
2502
+ );
2503
+ }}
2504
+ >
2632
2505
  {isSchemaExpanded ? (
2633
- <div className="sqlite-schema-content">
2634
- {tables.length > 0 ? (
2635
- <div className="sqlite-object-section">
2636
- <p className="sqlite-object-section-title">
2637
- Tables
2638
- </p>
2639
- <div className="sqlite-object-list">
2640
- {tables.map((entity) => {
2641
- const isSelected =
2642
- getEntityKey(
2643
- database.id,
2644
- entity.schemaName,
2645
- entity.name,
2646
- ) === selectedEntityKey;
2647
-
2648
- return (
2649
- <button
2650
- key={`${database.id}-${entity.schemaName}-${entity.name}`}
2651
- type="button"
2652
- className={joinClassNames(
2653
- 'sqlite-object-row',
2654
- isSelected &&
2655
- 'is-active',
2656
- )}
2657
- onClick={() => {
2658
- setEntitySelection(
2659
- database.id,
2660
- entity,
2661
- );
2662
- setActiveTab('data');
2663
- }}
2664
- >
2665
- <Table2
2666
- aria-hidden="true"
2667
- className="h-4 w-4 shrink-0"
2668
- />
2669
- <span className="min-w-0 flex-1 truncate text-left">
2670
- {entity.name}
2671
- </span>
2672
- </button>
2673
- );
2674
- })}
2675
- </div>
2506
+ <ChevronDown aria-hidden="true" className="h-4 w-4" />
2507
+ ) : (
2508
+ <ChevronRight aria-hidden="true" className="h-4 w-4" />
2509
+ )}
2510
+ <FolderTree aria-hidden="true" className="h-4 w-4" />
2511
+ <span className="min-w-0 flex-1 truncate">{schema.name}</span>
2512
+ </button>
2513
+
2514
+ {isSchemaExpanded ? (
2515
+ <div className="sqlite-schema-content">
2516
+ {tables.length > 0 ? (
2517
+ <div className="sqlite-object-section">
2518
+ <p className="sqlite-object-section-title">Tables</p>
2519
+ <div className="sqlite-object-list">
2520
+ {tables.map((entity) => {
2521
+ const isSelected =
2522
+ getEntityKey(
2523
+ database.id,
2524
+ entity.schemaName,
2525
+ entity.name,
2526
+ ) === selectedEntityKey;
2527
+
2528
+ return (
2529
+ <button
2530
+ key={`${database.id}-${entity.schemaName}-${entity.name}`}
2531
+ type="button"
2532
+ className={joinClassNames(
2533
+ 'sqlite-object-row',
2534
+ isSelected && 'is-active',
2535
+ )}
2536
+ onClick={() => {
2537
+ setEntitySelection(database.id, entity);
2538
+ setActiveTab('data');
2539
+ }}
2540
+ >
2541
+ <Table2
2542
+ aria-hidden="true"
2543
+ className="h-4 w-4 shrink-0"
2544
+ />
2545
+ <span className="min-w-0 flex-1 truncate text-left">
2546
+ {entity.name}
2547
+ </span>
2548
+ </button>
2549
+ );
2550
+ })}
2676
2551
  </div>
2677
- ) : null}
2678
-
2679
- {views.length > 0 ? (
2680
- <div className="sqlite-object-section">
2681
- <p className="sqlite-object-section-title">
2682
- Views
2683
- </p>
2684
- <div className="sqlite-object-list">
2685
- {views.map((entity) => {
2686
- const isSelected =
2687
- getEntityKey(
2688
- database.id,
2689
- entity.schemaName,
2690
- entity.name,
2691
- ) === selectedEntityKey;
2692
-
2693
- return (
2694
- <button
2695
- key={`${database.id}-${entity.schemaName}-${entity.name}`}
2696
- type="button"
2697
- className={joinClassNames(
2698
- 'sqlite-object-row',
2699
- isSelected &&
2700
- 'is-active',
2701
- )}
2702
- onClick={() => {
2703
- setEntitySelection(
2704
- database.id,
2705
- entity,
2706
- );
2707
- setActiveTab(
2708
- 'structure',
2709
- );
2710
- }}
2711
- >
2712
- <FileCode2
2713
- aria-hidden="true"
2714
- className="h-4 w-4 shrink-0"
2715
- />
2716
- <span className="min-w-0 flex-1 truncate text-left">
2717
- {entity.name}
2718
- </span>
2719
- </button>
2720
- );
2721
- })}
2722
- </div>
2552
+ </div>
2553
+ ) : null}
2554
+
2555
+ {views.length > 0 ? (
2556
+ <div className="sqlite-object-section">
2557
+ <p className="sqlite-object-section-title">Views</p>
2558
+ <div className="sqlite-object-list">
2559
+ {views.map((entity) => {
2560
+ const isSelected =
2561
+ getEntityKey(
2562
+ database.id,
2563
+ entity.schemaName,
2564
+ entity.name,
2565
+ ) === selectedEntityKey;
2566
+
2567
+ return (
2568
+ <button
2569
+ key={`${database.id}-${entity.schemaName}-${entity.name}`}
2570
+ type="button"
2571
+ className={joinClassNames(
2572
+ 'sqlite-object-row',
2573
+ isSelected && 'is-active',
2574
+ )}
2575
+ onClick={() => {
2576
+ setEntitySelection(database.id, entity);
2577
+ setActiveTab('structure');
2578
+ }}
2579
+ >
2580
+ <FileCode2
2581
+ aria-hidden="true"
2582
+ className="h-4 w-4 shrink-0"
2583
+ />
2584
+ <span className="min-w-0 flex-1 truncate text-left">
2585
+ {entity.name}
2586
+ </span>
2587
+ </button>
2588
+ );
2589
+ })}
2723
2590
  </div>
2724
- ) : null}
2725
- </div>
2726
- ) : null}
2727
- </div>
2728
- );
2729
- },
2730
- )
2591
+ </div>
2592
+ ) : null}
2593
+ </div>
2594
+ ) : null}
2595
+ </div>
2596
+ );
2597
+ })
2731
2598
  )}
2732
2599
  </div>
2733
2600
  ) : null}
@@ -2809,6 +2676,13 @@ export default function SqlitePanel() {
2809
2676
  onClose={() => setDeletingRow(null)}
2810
2677
  onDelete={handleDeleteRow}
2811
2678
  />
2679
+
2680
+ <SqliteDropModal
2681
+ isOpen={!!activeDrop}
2682
+ target={activeDrop?.target ?? DEFAULT_DROP_TARGET}
2683
+ onClose={() => setActiveDrop(null)}
2684
+ onConfirm={handleConfirmDrop}
2685
+ />
2812
2686
  </div>
2813
2687
  </div>
2814
2688
  );