@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.
- package/CHANGELOG.md +23 -0
- package/dist/devtools/assets/panel-BarPLQ4-.css +1 -0
- package/dist/devtools/assets/{panel-DjRs5NTl.js → panel-BsdCagVv.js} +45 -44
- package/dist/devtools/panel.html +2 -2
- package/dist/react-native/chunks/index.require.js +4 -16
- package/dist/react-native/chunks/sql.require.js +1 -3
- package/dist/react-native/chunks/useRozeniteSqlitePlugin.require.js +92 -110
- package/dist/react-native/index.d.ts +2 -2
- package/dist/rozenite.json +1 -1
- package/package.json +27 -31
- package/react-native.ts +7 -14
- package/src/react-native/adapters/__tests__/expo-sqlite.test.ts +4 -14
- package/src/react-native/adapters/expo-sqlite.ts +11 -40
- package/src/react-native/adapters/generic.ts +5 -13
- package/src/react-native/adapters/index.ts +1 -4
- package/src/react-native/sqlite-view.ts +2 -8
- package/src/react-native/useRozeniteSqlitePlugin.ts +110 -130
- package/src/react-native/useSqliteAgentTools.ts +5 -14
- package/src/shared/__tests__/bridge-values.test.ts +6 -11
- package/src/shared/__tests__/sql.test.ts +4 -8
- package/src/shared/bridge-values.ts +2 -5
- package/src/shared/sql.ts +3 -7
- package/src/ui/__tests__/sql-editor-utils.test.ts +1 -5
- package/src/ui/__tests__/sqlite-drop-mutations.test.ts +138 -0
- package/src/ui/__tests__/sqlite-row-mutations.test.ts +1 -3
- package/src/ui/__tests__/sqlite-table-column-order.test.ts +4 -19
- package/src/ui/cell-detail-drawer.tsx +1 -6
- package/src/ui/globals.css +26 -65
- package/src/ui/panel.tsx +442 -568
- package/src/ui/query-result-table.tsx +8 -26
- package/src/ui/sql-editor-utils.ts +24 -55
- package/src/ui/sql-editor.tsx +13 -46
- package/src/ui/sqlite-data-table.tsx +11 -28
- package/src/ui/sqlite-drop-modal.tsx +193 -0
- package/src/ui/sqlite-drop-mutations.ts +53 -0
- package/src/ui/sqlite-introspection.ts +9 -25
- package/src/ui/sqlite-row-delete-modal.tsx +5 -12
- package/src/ui/sqlite-row-edit-modal.tsx +22 -64
- package/src/ui/sqlite-row-edit-value.ts +2 -11
- package/src/ui/sqlite-row-mutations.ts +10 -32
- package/src/ui/sqlite-table-column-order.ts +8 -23
- package/src/ui/use-sqlite-requests.ts +46 -86
- package/src/ui/utils.ts +1 -5
- package/src/ui/value-utils.tsx +3 -11
- package/dist/devtools/assets/panel-CIU0JBOs.css +0 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
350
|
-
) ?? entityMatches[0]
|
|
318
|
+
entityMatches.find((entity) => normalizeIdentifier(entity.schemaName) === 'main') ??
|
|
319
|
+
entityMatches[0]
|
|
351
320
|
);
|
|
352
321
|
};
|
package/src/ui/sql-editor.tsx
CHANGED
|
@@ -5,26 +5,11 @@ import {
|
|
|
5
5
|
completionKeymap,
|
|
6
6
|
type CompletionSource,
|
|
7
7
|
} from '@codemirror/autocomplete';
|
|
8
|
-
import {
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
};
|