@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
@@ -2,11 +2,7 @@ import { useMemo, useState, type ReactNode } from 'react';
2
2
  import type { CellContext, ColumnDef, OnChangeFn } from '@tanstack/react-table';
3
3
  import type { SqliteQueryResult } from '../shared/types';
4
4
  import { formatDuration, formatNumber } from './utils';
5
- import {
6
- getMetadataBadgeClassName,
7
- getValueKind,
8
- getValuePreview,
9
- } from './value-utils';
5
+ import { getMetadataBadgeClassName, getValueKind, getValuePreview } from './value-utils';
10
6
  import { CellDetailDrawer } from './cell-detail-drawer';
11
7
  import { SqliteDataTable } from './sqlite-data-table';
12
8
 
@@ -44,9 +40,8 @@ type DrawerPayload = {
44
40
  value: Record<string, unknown>;
45
41
  } | null;
46
42
 
47
- const joinClassNames = (
48
- ...classNames: Array<string | false | null | undefined>
49
- ) => classNames.filter(Boolean).join(' ');
43
+ const joinClassNames = (...classNames: Array<string | false | null | undefined>) =>
44
+ classNames.filter(Boolean).join(' ');
50
45
 
51
46
  const getColumnHeaderTitle = (
52
47
  column: string,
@@ -95,9 +90,7 @@ export const QueryResultTable = ({
95
90
  const handleInspectRow = (row: Record<string, unknown>, rowIndex: number) => {
96
91
  setDrawerPayload({
97
92
  title: `Row ${rowNumberOffset + rowIndex + 1}`,
98
- value: Object.fromEntries(
99
- visibleColumns.map((column) => [column, row[column]]),
100
- ),
93
+ value: Object.fromEntries(visibleColumns.map((column) => [column, row[column]])),
101
94
  });
102
95
  };
103
96
 
@@ -106,9 +99,7 @@ export const QueryResultTable = ({
106
99
  ...visibleColumns.map((column) => ({
107
100
  id: column,
108
101
  header: () => (
109
- <span title={getColumnHeaderTitle(column, columnMeta?.[column])}>
110
- {column}
111
- </span>
102
+ <span title={getColumnHeaderTitle(column, columnMeta?.[column])}>{column}</span>
112
103
  ),
113
104
  accessorFn: (row: Record<string, unknown>) => row[column],
114
105
  cell: ({ row }: CellContext<Record<string, unknown>, unknown>) => {
@@ -116,9 +107,7 @@ export const QueryResultTable = ({
116
107
 
117
108
  return (
118
109
  <div className="sqlite-cell-value">
119
- <span className="sqlite-cell-preview">
120
- {getValuePreview(value)}
121
- </span>
110
+ <span className="sqlite-cell-preview">{getValuePreview(value)}</span>
122
111
  <span className="sqlite-cell-kind">{getValueKind(value)}</span>
123
112
  </div>
124
113
  );
@@ -145,12 +134,7 @@ export const QueryResultTable = ({
145
134
  <>
146
135
  {showMetadata && metadata ? (
147
136
  <div className="sqlite-inline-metadata">
148
- <span
149
- className={joinClassNames(
150
- 'sqlite-badge',
151
- getMetadataBadgeClassName(metadata),
152
- )}
153
- >
137
+ <span className={joinClassNames('sqlite-badge', getMetadataBadgeClassName(metadata))}>
154
138
  {metadata.statementType}
155
139
  </span>
156
140
  <span className="sqlite-inline-stat sqlite-tabular">
@@ -183,9 +167,7 @@ export const QueryResultTable = ({
183
167
  showRowNumbers
184
168
  rowNumberOffset={rowNumberOffset}
185
169
  onRowClick={handleInspectRow}
186
- getRowAriaLabel={(_, rowIndex) =>
187
- `Inspect row ${rowNumberOffset + rowIndex + 1}`
188
- }
170
+ getRowAriaLabel={(_, rowIndex) => `Inspect row ${rowNumberOffset + rowIndex + 1}`}
189
171
  />
190
172
 
191
173
  <CellDetailDrawer
@@ -2,11 +2,7 @@ import type { Completion } from '@codemirror/autocomplete';
2
2
  import type { SQLNamespace } from '@codemirror/lang-sql';
3
3
  import { format } from 'sql-formatter';
4
4
  import { quoteSqlIdentifier } from '../shared/sql';
5
- import type {
6
- SqliteColumnInfo,
7
- SqliteEntity,
8
- SqliteSchema,
9
- } from './sqlite-introspection';
5
+ import type { SqliteColumnInfo, SqliteEntity, SqliteSchema } from './sqlite-introspection';
10
6
 
11
7
  export type SqlEditorColumnCacheState = {
12
8
  databaseId: string | null;
@@ -28,17 +24,14 @@ type SqlEditorAliasLookup = Record<
28
24
  }
29
25
  >;
30
26
 
31
- const SQL_IDENTIFIER_PATTERN =
32
- '"(?:[^"]|"")+"|`(?:[^`]|``)+`|\\[[^\\]]+\\]|[A-Za-z_][\\w$]*';
27
+ const SQL_IDENTIFIER_PATTERN = '"(?:[^"]|"")+"|`(?:[^`]|``)+`|\\[[^\\]]+\\]|[A-Za-z_][\\w$]*';
33
28
 
34
29
  const bareIdentifierPattern = /^[A-Za-z_][\w$]*$/;
35
30
  const trailingIdentifierPattern = /[A-Za-z_][\w$]*$/;
36
31
  const entityMemberPattern = new RegExp(
37
32
  `(${SQL_IDENTIFIER_PATTERN})\\s*\\.\\s*(${SQL_IDENTIFIER_PATTERN})\\s*\\.\\s*$`,
38
33
  );
39
- const singleMemberPattern = new RegExp(
40
- `(${SQL_IDENTIFIER_PATTERN})\\s*\\.\\s*$`,
41
- );
34
+ const singleMemberPattern = new RegExp(`(${SQL_IDENTIFIER_PATTERN})\\s*\\.\\s*$`);
42
35
  const aliasPattern = new RegExp(
43
36
  `\\b(?:FROM|JOIN|UPDATE|INTO)\\s+(?:(?:(${SQL_IDENTIFIER_PATTERN})\\s*\\.\\s*)?(${SQL_IDENTIFIER_PATTERN}))(?:\\s+(?:AS\\s+)?(${SQL_IDENTIFIER_PATTERN}))?`,
44
37
  'gi',
@@ -63,9 +56,7 @@ const unquoteIdentifier = (identifier: string) => {
63
56
  };
64
57
 
65
58
  const getIdentifierInsertText = (identifier: string) =>
66
- bareIdentifierPattern.test(identifier)
67
- ? identifier
68
- : quoteSqlIdentifier(identifier);
59
+ bareIdentifierPattern.test(identifier) ? identifier : quoteSqlIdentifier(identifier);
69
60
 
70
61
  const getColumnDetail = (column: SqliteColumnInfo) => {
71
62
  const parts = [column.type].filter(Boolean);
@@ -95,10 +86,7 @@ export const createSqlEditorColumnCache = (
95
86
  export const syncSqlEditorColumnCacheDatabase = (
96
87
  state: SqlEditorColumnCacheState,
97
88
  databaseId: string | null,
98
- ) =>
99
- state.databaseId === databaseId
100
- ? state
101
- : createSqlEditorColumnCache(databaseId);
89
+ ) => (state.databaseId === databaseId ? state : createSqlEditorColumnCache(databaseId));
102
90
 
103
91
  export const getSqlEditorColumnCacheKey = (
104
92
  databaseId: string,
@@ -111,8 +99,7 @@ export const getSqlEditorCachedColumns = (
111
99
  databaseId: string,
112
100
  schemaName: string,
113
101
  entityName: string,
114
- ) =>
115
- state.entries[getSqlEditorColumnCacheKey(databaseId, schemaName, entityName)];
102
+ ) => state.entries[getSqlEditorColumnCacheKey(databaseId, schemaName, entityName)];
116
103
 
117
104
  export const setSqlEditorCachedColumns = (
118
105
  state: SqlEditorColumnCacheState,
@@ -176,12 +163,8 @@ export const buildSqlCompletionSchema = ({
176
163
  const columns =
177
164
  databaseId == null
178
165
  ? []
179
- : (getSqlEditorCachedColumns(
180
- columnCache,
181
- databaseId,
182
- entity.schemaName,
183
- entity.name,
184
- ) ?? []);
166
+ : (getSqlEditorCachedColumns(columnCache, databaseId, entity.schemaName, entity.name) ??
167
+ []);
185
168
 
186
169
  schemaChildren[entity.name] = {
187
170
  self: {
@@ -220,19 +203,15 @@ export const getDefaultSqlCompletionSchema = (schemas: SqliteSchema[]) =>
220
203
  schemas.find((schema) => schema.name === 'main')?.name ?? schemas[0]?.name;
221
204
 
222
205
  export const createSqlColumnCompletions = (columns: SqliteColumnInfo[]) =>
223
- columns.map(
224
- (column): Completion => ({
225
- label: column.name,
226
- apply: getIdentifierInsertText(column.name),
227
- detail: getColumnDetail(column) || undefined,
228
- type: 'property',
229
- boost: column.primaryKeyOrder > 0 ? 1 : 0,
230
- }),
231
- );
232
-
233
- export const extractSqlEditorAliases = (
234
- sqlBeforeCursor: string,
235
- ): SqlEditorAliasLookup => {
206
+ columns.map((column): Completion => ({
207
+ label: column.name,
208
+ apply: getIdentifierInsertText(column.name),
209
+ detail: getColumnDetail(column) || undefined,
210
+ type: 'property',
211
+ boost: column.primaryKeyOrder > 0 ? 1 : 0,
212
+ }));
213
+
214
+ export const extractSqlEditorAliases = (sqlBeforeCursor: string): SqlEditorAliasLookup => {
236
215
  const aliases: SqlEditorAliasLookup = {};
237
216
 
238
217
  let match: RegExpExecArray | null;
@@ -256,10 +235,7 @@ export const getSqlEditorColumnCompletionRequest = (
256
235
  const beforeCursor = sql.slice(0, cursorPosition);
257
236
  const trailingIdentifier = beforeCursor.match(trailingIdentifierPattern)?.[0];
258
237
  const replacementLength = trailingIdentifier?.length ?? 0;
259
- const lookupPrefix = beforeCursor.slice(
260
- 0,
261
- beforeCursor.length - replacementLength,
262
- );
238
+ const lookupPrefix = beforeCursor.slice(0, beforeCursor.length - replacementLength);
263
239
 
264
240
  const qualifiedMatch = lookupPrefix.match(entityMemberPattern);
265
241
  if (qualifiedMatch) {
@@ -298,8 +274,7 @@ export const resolveSqlEditorEntityReference = ({
298
274
  const findExactEntity = (schemaName: string, entityName: string) =>
299
275
  entities.find(
300
276
  (entity) =>
301
- normalizeIdentifier(entity.schemaName) ===
302
- normalizeIdentifier(schemaName) &&
277
+ normalizeIdentifier(entity.schemaName) === normalizeIdentifier(schemaName) &&
303
278
  normalizeIdentifier(entity.name) === normalizeIdentifier(entityName),
304
279
  ) ?? null;
305
280
 
@@ -315,17 +290,13 @@ export const resolveSqlEditorEntityReference = ({
315
290
 
316
291
  return (
317
292
  entities.find(
318
- (entity) =>
319
- normalizeIdentifier(entity.name) ===
320
- normalizeIdentifier(aliasMatch.entityName),
293
+ (entity) => normalizeIdentifier(entity.name) === normalizeIdentifier(aliasMatch.entityName),
321
294
  ) ?? null
322
295
  );
323
296
  }
324
297
 
325
298
  const entityMatches = entities.filter(
326
- (entity) =>
327
- normalizeIdentifier(entity.name) ===
328
- normalizeIdentifier(request.entityName),
299
+ (entity) => normalizeIdentifier(entity.name) === normalizeIdentifier(request.entityName),
329
300
  );
330
301
 
331
302
  if (entityMatches.length === 0) {
@@ -335,8 +306,7 @@ export const resolveSqlEditorEntityReference = ({
335
306
  if (selectedSchemaName) {
336
307
  const schemaMatch = entityMatches.find(
337
308
  (entity) =>
338
- normalizeIdentifier(entity.schemaName) ===
339
- normalizeIdentifier(selectedSchemaName),
309
+ normalizeIdentifier(entity.schemaName) === normalizeIdentifier(selectedSchemaName),
340
310
  );
341
311
 
342
312
  if (schemaMatch) {
@@ -345,8 +315,7 @@ export const resolveSqlEditorEntityReference = ({
345
315
  }
346
316
 
347
317
  return (
348
- entityMatches.find(
349
- (entity) => normalizeIdentifier(entity.schemaName) === 'main',
350
- ) ?? entityMatches[0]
318
+ entityMatches.find((entity) => normalizeIdentifier(entity.schemaName) === 'main') ??
319
+ entityMatches[0]
351
320
  );
352
321
  };
@@ -5,26 +5,11 @@ import {
5
5
  completionKeymap,
6
6
  type CompletionSource,
7
7
  } from '@codemirror/autocomplete';
8
- import {
9
- defaultKeymap,
10
- history,
11
- historyKeymap,
12
- indentWithTab,
13
- } from '@codemirror/commands';
14
- import {
15
- schemaCompletionSource,
16
- sql,
17
- SQLite,
18
- type SQLNamespace,
19
- } from '@codemirror/lang-sql';
8
+ import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands';
9
+ import { schemaCompletionSource, sql, SQLite, type SQLNamespace } from '@codemirror/lang-sql';
20
10
  import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
21
11
  import { tags } from '@lezer/highlight';
22
- import {
23
- Compartment,
24
- EditorSelection,
25
- EditorState,
26
- type Extension,
27
- } from '@codemirror/state';
12
+ import { Compartment, EditorSelection, EditorState, type Extension } from '@codemirror/state';
28
13
  import { searchKeymap } from '@codemirror/search';
29
14
  import {
30
15
  Decoration,
@@ -36,13 +21,7 @@ import {
36
21
  lineNumbers,
37
22
  placeholder,
38
23
  } from '@codemirror/view';
39
- import {
40
- forwardRef,
41
- useEffect,
42
- useImperativeHandle,
43
- useRef,
44
- type Ref,
45
- } from 'react';
24
+ import { forwardRef, useEffect, useImperativeHandle, useRef, type Ref } from 'react';
46
25
 
47
26
  export type SqlEditorHandle = {
48
27
  focus: () => void;
@@ -137,8 +116,7 @@ const sqlEditorTheme = EditorView.theme(
137
116
  display: 'flex',
138
117
  flex: '1 1 auto',
139
118
  minHeight: '0',
140
- fontFamily:
141
- "'IBM Plex Mono', 'JetBrains Mono', 'SFMono-Regular', ui-monospace, monospace",
119
+ fontFamily: "'IBM Plex Mono', 'JetBrains Mono', 'SFMono-Regular', ui-monospace, monospace",
142
120
  },
143
121
  '.cm-sizer': {
144
122
  minHeight: '100%',
@@ -152,10 +130,9 @@ const sqlEditorTheme = EditorView.theme(
152
130
  '.cm-cursor, .cm-dropCursor': {
153
131
  borderLeftColor: '#f7fbff',
154
132
  },
155
- '.cm-selectionBackground, &.cm-focused .cm-selectionBackground, ::selection':
156
- {
157
- backgroundColor: 'rgba(89, 163, 255, 0.2)',
158
- },
133
+ '.cm-selectionBackground, &.cm-focused .cm-selectionBackground, ::selection': {
134
+ backgroundColor: 'rgba(89, 163, 255, 0.2)',
135
+ },
159
136
  '.cm-activeLine': {
160
137
  backgroundColor: 'rgba(255, 255, 255, 0.032)',
161
138
  },
@@ -367,9 +344,7 @@ const SqlEditorInner = (
367
344
  history(),
368
345
  closeBrackets(),
369
346
  syntaxHighlighting(sqlHighlightStyle),
370
- sqlSupportCompartment.of(
371
- sql({ dialect: SQLite, upperCaseKeywords: true }),
372
- ),
347
+ sqlSupportCompartment.of(sql({ dialect: SQLite, upperCaseKeywords: true })),
373
348
  autocompleteCompartment.of(
374
349
  createAutocompleteExtension({
375
350
  completionSchema: initialConfigRef.current.completionSchema,
@@ -378,15 +353,9 @@ const SqlEditorInner = (
378
353
  defaultTable: initialConfigRef.current.defaultTable,
379
354
  }),
380
355
  ),
381
- errorLineCompartment.of(
382
- createErrorLineExtension(initialConfigRef.current.errorLine),
383
- ),
384
- placeholderCompartment.of(
385
- placeholder(initialConfigRef.current.placeholderText),
386
- ),
387
- editableCompartment.of(
388
- createEditableExtension(initialConfigRef.current.readOnly),
389
- ),
356
+ errorLineCompartment.of(createErrorLineExtension(initialConfigRef.current.errorLine)),
357
+ placeholderCompartment.of(placeholder(initialConfigRef.current.placeholderText)),
358
+ editableCompartment.of(createEditableExtension(initialConfigRef.current.readOnly)),
390
359
  keymap.of([
391
360
  {
392
361
  key: 'Mod-Enter',
@@ -447,9 +416,7 @@ const SqlEditorInner = (
447
416
 
448
417
  editorView.dispatch({
449
418
  effects: [
450
- sqlSupportCompartment.reconfigure(
451
- sql({ dialect: SQLite, upperCaseKeywords: true }),
452
- ),
419
+ sqlSupportCompartment.reconfigure(sql({ dialect: SQLite, upperCaseKeywords: true })),
453
420
  autocompleteCompartment.reconfigure(
454
421
  createAutocompleteExtension({
455
422
  completionSchema,
@@ -19,9 +19,8 @@ import {
19
19
  import { formatNumber } from './utils';
20
20
  import { SQLITE_ROW_NUMBER_COLUMN_ID } from './sqlite-table-column-order';
21
21
 
22
- const joinClassNames = (
23
- ...classNames: Array<string | false | null | undefined>
24
- ) => classNames.filter(Boolean).join(' ');
22
+ const joinClassNames = (...classNames: Array<string | false | null | undefined>) =>
23
+ classNames.filter(Boolean).join(' ');
25
24
 
26
25
  const LoadingState = ({ columns }: { columns: number }) => (
27
26
  <div className="sqlite-results-loading" aria-live="polite">
@@ -34,10 +33,7 @@ const LoadingState = ({ columns }: { columns: number }) => (
34
33
  }}
35
34
  >
36
35
  {Array.from({ length: Math.max(columns, 3) }, (_, columnIndex) => (
37
- <span
38
- key={`${rowIndex}-${columnIndex}`}
39
- className="sqlite-results-loading-bar"
40
- />
36
+ <span key={`${rowIndex}-${columnIndex}`} className="sqlite-results-loading-bar" />
41
37
  ))}
42
38
  </div>
43
39
  ))}
@@ -74,8 +70,7 @@ const SortableColumnHeader = <TData extends RowData>({
74
70
  <th
75
71
  scope="col"
76
72
  className={joinClassNames(
77
- header.column.id === SQLITE_ROW_NUMBER_COLUMN_ID &&
78
- 'sqlite-results-number-col',
73
+ header.column.id === SQLITE_ROW_NUMBER_COLUMN_ID && 'sqlite-results-number-col',
79
74
  header.column.getIsResizing() && 'sqlite-table-column-resizing',
80
75
  )}
81
76
  style={{
@@ -165,8 +160,7 @@ export const SqliteDataTable = <TData extends RowData>({
165
160
  getCoreRowModel: getCoreRowModel(),
166
161
  });
167
162
 
168
- const loadingColumns =
169
- loadingColumnCount ?? columns.length + (showRowNumbers ? 1 : 0);
163
+ const loadingColumns = loadingColumnCount ?? columns.length + (showRowNumbers ? 1 : 0);
170
164
  const tableRows = table.getRowModel().rows;
171
165
  const rowVirtualizer = useVirtualizer({
172
166
  count: tableRows.length,
@@ -175,8 +169,7 @@ export const SqliteDataTable = <TData extends RowData>({
175
169
  overscan: 10,
176
170
  getItemKey: (index) => tableRows[index]?.id ?? index,
177
171
  measureElement:
178
- typeof window !== 'undefined' &&
179
- !window.navigator.userAgent.includes('Firefox')
172
+ typeof window !== 'undefined' && !window.navigator.userAgent.includes('Firefox')
180
173
  ? (element) => element?.getBoundingClientRect().height ?? 0
181
174
  : undefined,
182
175
  });
@@ -235,14 +228,11 @@ export const SqliteDataTable = <TData extends RowData>({
235
228
  tabIndex={onRowClick ? 0 : undefined}
236
229
  aria-label={
237
230
  onRowClick
238
- ? (getRowAriaLabel?.(row.original, row.index) ??
239
- `Inspect row ${row.index + 1}`)
231
+ ? (getRowAriaLabel?.(row.original, row.index) ?? `Inspect row ${row.index + 1}`)
240
232
  : undefined
241
233
  }
242
234
  onClick={() => onRowClick?.(row.original, row.index)}
243
- onKeyDown={(event) =>
244
- handleRowKeyDown(event, row.original, row.index)
245
- }
235
+ onKeyDown={(event) => handleRowKeyDown(event, row.original, row.index)}
246
236
  style={{
247
237
  transform: `translateY(${virtualRow.start}px)`,
248
238
  }}
@@ -251,8 +241,7 @@ export const SqliteDataTable = <TData extends RowData>({
251
241
  <td
252
242
  key={cell.id}
253
243
  className={joinClassNames(
254
- cell.column.id === SQLITE_ROW_NUMBER_COLUMN_ID &&
255
- 'sqlite-results-row-number',
244
+ cell.column.id === SQLITE_ROW_NUMBER_COLUMN_ID && 'sqlite-results-row-number',
256
245
  )}
257
246
  style={{ width: cell.column.getSize() }}
258
247
  >
@@ -267,16 +256,10 @@ export const SqliteDataTable = <TData extends RowData>({
267
256
  );
268
257
 
269
258
  return (
270
- <div
271
- className={joinClassNames('sqlite-results-shell', shellClassName)}
272
- data-table-id={tableId}
273
- >
259
+ <div className={joinClassNames('sqlite-results-shell', shellClassName)} data-table-id={tableId}>
274
260
  <div
275
261
  ref={scrollElementRef}
276
- className={joinClassNames(
277
- 'sqlite-results-scroll',
278
- scrollContainerClassName,
279
- )}
262
+ className={joinClassNames('sqlite-results-scroll', scrollContainerClassName)}
280
263
  >
281
264
  {loading ? (
282
265
  <LoadingState columns={loadingColumns} />
@@ -0,0 +1,193 @@
1
+ import { Modal, useOverlayState } from '@heroui/react';
2
+ import { AlertTriangle, Trash2 } from 'lucide-react';
3
+ import { useEffect, useState } from 'react';
4
+ import { SqliteModalCloseButton, sqliteSecondaryButtonClassName } from './sqlite-modal-controls';
5
+
6
+ export type SqliteDropModalTarget =
7
+ | {
8
+ kind: 'entity';
9
+ entityType: 'table' | 'view';
10
+ qualifiedName: string;
11
+ confirmationValue: string;
12
+ sql: string;
13
+ }
14
+ | {
15
+ kind: 'database';
16
+ databaseName: string;
17
+ confirmationValue: string;
18
+ tableCount: number;
19
+ viewCount: number;
20
+ sql: string;
21
+ };
22
+
23
+ type SqliteDropModalProps = {
24
+ isOpen: boolean;
25
+ target: SqliteDropModalTarget;
26
+ onClose: () => void;
27
+ onConfirm: () => Promise<void>;
28
+ };
29
+
30
+ const toneButtonClassName =
31
+ 'sqlite-button inline-flex items-center justify-center gap-2 rounded-xl px-3 py-2 text-sm font-medium';
32
+ const dangerButtonClassName = `${toneButtonClassName} border border-rose-400/30 bg-rose-500/16 text-rose-50 hover:bg-rose-500/24`;
33
+
34
+ const getModalTitle = (target: SqliteDropModalTarget) =>
35
+ target.kind === 'entity'
36
+ ? `Drop ${target.entityType === 'view' ? 'View' : 'Table'}`
37
+ : 'Drop All Objects';
38
+
39
+ const getModalSubtitle = (target: SqliteDropModalTarget) =>
40
+ target.kind === 'entity' ? target.qualifiedName : target.databaseName;
41
+
42
+ const getConfirmationLabel = (target: SqliteDropModalTarget) =>
43
+ target.kind === 'entity'
44
+ ? `${target.entityType === 'view' ? 'view' : 'table'} name`
45
+ : 'database name';
46
+
47
+ export const SqliteDropModal = ({ isOpen, target, onClose, onConfirm }: SqliteDropModalProps) => {
48
+ const overlay = useOverlayState({
49
+ isOpen,
50
+ onOpenChange: (open: boolean) => {
51
+ if (!open) {
52
+ onClose();
53
+ }
54
+ },
55
+ });
56
+ const [dropping, setDropping] = useState(false);
57
+ const [error, setError] = useState<string | null>(null);
58
+ const [confirmationInput, setConfirmationInput] = useState('');
59
+
60
+ useEffect(() => {
61
+ if (isOpen) {
62
+ setDropping(false);
63
+ setError(null);
64
+ setConfirmationInput('');
65
+ }
66
+ }, [isOpen]);
67
+
68
+ const isConfirmed = confirmationInput === target.confirmationValue;
69
+
70
+ const handleConfirm = async () => {
71
+ if (!isConfirmed) {
72
+ return;
73
+ }
74
+
75
+ try {
76
+ setDropping(true);
77
+ setError(null);
78
+ await onConfirm();
79
+ onClose();
80
+ } catch (nextError) {
81
+ setError(nextError instanceof Error ? nextError.message : String(nextError));
82
+ } finally {
83
+ setDropping(false);
84
+ }
85
+ };
86
+
87
+ return (
88
+ <Modal state={overlay}>
89
+ <Modal.Backdrop
90
+ variant="blur"
91
+ isDismissable={!dropping}
92
+ className="bg-[rgba(5,10,16,0.24)] backdrop-blur-[2px]"
93
+ >
94
+ <Modal.Container placement="center" size="md" scroll="inside">
95
+ <Modal.Dialog
96
+ aria-label={`${getModalTitle(target)} ${getModalSubtitle(target)}`}
97
+ className="w-full max-w-xl overflow-hidden border border-white/10 bg-[#0a121b] p-0 text-white shadow-[0_30px_90px_rgba(0,0,0,0.42)]"
98
+ >
99
+ <div className="flex items-center justify-between gap-4 border-b border-white/8 px-5 py-5">
100
+ <div className="flex items-center gap-3">
101
+ <div className="flex h-10 w-10 items-center justify-center rounded-full bg-rose-500/12 text-rose-200">
102
+ <AlertTriangle aria-hidden="true" className="h-5 w-5" />
103
+ </div>
104
+ <div>
105
+ <h2 className="text-lg font-semibold text-white">{getModalTitle(target)}</h2>
106
+ <p className="mt-1 text-sm text-slate-400">{getModalSubtitle(target)}</p>
107
+ </div>
108
+ </div>
109
+ <SqliteModalCloseButton onClose={onClose} disabled={dropping} />
110
+ </div>
111
+
112
+ <Modal.Body className="space-y-0 p-0">
113
+ <div className="space-y-5 px-5 py-5">
114
+ {target.kind === 'database' ? (
115
+ <p className="text-sm leading-6 text-slate-300">
116
+ This drops every table and view in{' '}
117
+ <span className="font-medium text-white">{target.databaseName}</span> —{' '}
118
+ {target.tableCount} table
119
+ {target.tableCount === 1 ? '' : 's'} and {target.viewCount} view
120
+ {target.viewCount === 1 ? '' : 's'}, along with all their rows, indexes and
121
+ triggers. The database file itself is not deleted; it is left empty. This cannot
122
+ be undone.
123
+ </p>
124
+ ) : (
125
+ <p className="text-sm leading-6 text-slate-300">
126
+ This permanently drops{' '}
127
+ <span className="font-medium text-white">{target.qualifiedName}</span> and
128
+ everything it owns — rows, indexes and triggers. This cannot be undone.
129
+ </p>
130
+ )}
131
+
132
+ <div className="space-y-2">
133
+ <p className="sqlite-helper-text">SQL to be executed</p>
134
+ <pre className="max-h-40 overflow-auto rounded-lg border border-white/8 bg-black/30 px-3 py-2 text-xs leading-5 text-slate-200">
135
+ {target.sql}
136
+ </pre>
137
+ </div>
138
+
139
+ <div className="space-y-2">
140
+ <label htmlFor="sqlite-drop-confirmation-input" className="sqlite-helper-text">
141
+ Type the {getConfirmationLabel(target)}{' '}
142
+ <span className="font-medium text-white">{target.confirmationValue}</span> to
143
+ confirm
144
+ </label>
145
+ <input
146
+ id="sqlite-drop-confirmation-input"
147
+ type="text"
148
+ name="dropConfirmation"
149
+ autoComplete="off"
150
+ spellCheck={false}
151
+ className="sqlite-input"
152
+ value={confirmationInput}
153
+ onChange={(event) => setConfirmationInput(event.target.value)}
154
+ disabled={dropping}
155
+ />
156
+ </div>
157
+
158
+ {error ? (
159
+ <div className="sqlite-inline-error" aria-live="polite">
160
+ <div>
161
+ <p className="font-medium text-rose-100">Drop Failed</p>
162
+ <p className="mt-1 text-sm text-rose-100/90">{error}</p>
163
+ </div>
164
+ </div>
165
+ ) : null}
166
+ </div>
167
+
168
+ <div className="flex items-center justify-end gap-3 border-t border-white/8 px-5 py-5">
169
+ <button
170
+ type="button"
171
+ className={sqliteSecondaryButtonClassName}
172
+ onClick={onClose}
173
+ disabled={dropping}
174
+ >
175
+ Cancel
176
+ </button>
177
+ <button
178
+ type="button"
179
+ className={dangerButtonClassName}
180
+ onClick={() => void handleConfirm()}
181
+ disabled={dropping || !isConfirmed}
182
+ >
183
+ <Trash2 aria-hidden="true" className="h-4 w-4" />
184
+ {dropping ? 'Dropping…' : getModalTitle(target)}
185
+ </button>
186
+ </div>
187
+ </Modal.Body>
188
+ </Modal.Dialog>
189
+ </Modal.Container>
190
+ </Modal.Backdrop>
191
+ </Modal>
192
+ );
193
+ };