@rozenite/sqlite-plugin 1.7.0-rc.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 (56) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/LICENSE +20 -0
  3. package/README.md +102 -0
  4. package/dist/devtools/assets/panel-B3paLkwG.js +82 -0
  5. package/dist/devtools/assets/panel-CIU0JBOs.css +1 -0
  6. package/dist/devtools/panel.html +31 -0
  7. package/dist/react-native/chunks/bridge-values.cjs +5 -0
  8. package/dist/react-native/chunks/bridge-values.js +258 -0
  9. package/dist/react-native/chunks/index.require.cjs +1 -0
  10. package/dist/react-native/chunks/index.require.js +118 -0
  11. package/dist/react-native/chunks/useRozeniteSqlitePlugin.require.cjs +1 -0
  12. package/dist/react-native/chunks/useRozeniteSqlitePlugin.require.js +189 -0
  13. package/dist/react-native/index.cjs +1 -0
  14. package/dist/react-native/index.d.ts +178 -0
  15. package/dist/react-native/index.js +16 -0
  16. package/dist/rozenite.json +1 -0
  17. package/package.json +83 -0
  18. package/postcss.config.js +6 -0
  19. package/react-native.ts +55 -0
  20. package/rozenite.config.ts +8 -0
  21. package/src/react-native/adapters/__tests__/expo-sqlite.test.ts +94 -0
  22. package/src/react-native/adapters/expo-sqlite.ts +230 -0
  23. package/src/react-native/adapters/generic.ts +88 -0
  24. package/src/react-native/adapters/index.ts +9 -0
  25. package/src/react-native/sqlite-view.ts +24 -0
  26. package/src/react-native/useRozeniteSqlitePlugin.ts +262 -0
  27. package/src/shared/__tests__/bridge-values.test.ts +34 -0
  28. package/src/shared/__tests__/sql.test.ts +55 -0
  29. package/src/shared/bridge-values.ts +170 -0
  30. package/src/shared/protocol.ts +41 -0
  31. package/src/shared/sql.ts +420 -0
  32. package/src/shared/types.ts +81 -0
  33. package/src/ui/__tests__/sql-editor-utils.test.ts +135 -0
  34. package/src/ui/__tests__/sqlite-row-edit-value.test.ts +22 -0
  35. package/src/ui/__tests__/sqlite-row-mutations.test.ts +310 -0
  36. package/src/ui/__tests__/sqlite-table-column-order.test.ts +83 -0
  37. package/src/ui/__tests__/value-utils.test.tsx +12 -0
  38. package/src/ui/cell-detail-drawer.tsx +65 -0
  39. package/src/ui/globals.css +1415 -0
  40. package/src/ui/panel.tsx +2815 -0
  41. package/src/ui/query-result-table.tsx +199 -0
  42. package/src/ui/sql-editor-utils.ts +352 -0
  43. package/src/ui/sql-editor.tsx +509 -0
  44. package/src/ui/sqlite-data-table.tsx +296 -0
  45. package/src/ui/sqlite-introspection.ts +189 -0
  46. package/src/ui/sqlite-modal-controls.tsx +32 -0
  47. package/src/ui/sqlite-row-delete-modal.tsx +130 -0
  48. package/src/ui/sqlite-row-edit-modal.tsx +487 -0
  49. package/src/ui/sqlite-row-edit-value.ts +53 -0
  50. package/src/ui/sqlite-row-mutations.ts +246 -0
  51. package/src/ui/sqlite-table-column-order.ts +154 -0
  52. package/src/ui/use-sqlite-requests.ts +205 -0
  53. package/src/ui/utils.ts +107 -0
  54. package/src/ui/value-utils.tsx +162 -0
  55. package/tsconfig.json +36 -0
  56. package/vite.config.ts +20 -0
@@ -0,0 +1,199 @@
1
+ import { useMemo, useState, type ReactNode } from 'react';
2
+ import type { CellContext, ColumnDef, OnChangeFn } from '@tanstack/react-table';
3
+ import type { SqliteQueryResult } from '../shared/types';
4
+ import { formatDuration, formatNumber } from './utils';
5
+ import {
6
+ getMetadataBadgeClassName,
7
+ getValueKind,
8
+ getValuePreview,
9
+ } from './value-utils';
10
+ import { CellDetailDrawer } from './cell-detail-drawer';
11
+ import { SqliteDataTable } from './sqlite-data-table';
12
+
13
+ type QueryResultTableProps = {
14
+ tableId: string;
15
+ result: SqliteQueryResult | null;
16
+ columnOrder: string[];
17
+ onColumnOrderChange: OnChangeFn<string[]>;
18
+ emptyTitle: string;
19
+ emptyDescription: string;
20
+ loading?: boolean;
21
+ showMetadata?: boolean;
22
+ tableClassName?: string;
23
+ shellClassName?: string;
24
+ scrollContainerClassName?: string;
25
+ rowNumberOffset?: number;
26
+ columnMeta?: Record<
27
+ string,
28
+ {
29
+ type?: string | null;
30
+ isPrimaryKey?: boolean;
31
+ isForeignKey?: boolean;
32
+ }
33
+ >;
34
+ hiddenColumnIds?: string[];
35
+ rowActions?: {
36
+ columnId: string;
37
+ header: string;
38
+ cell: (row: Record<string, unknown>, rowIndex: number) => ReactNode;
39
+ };
40
+ };
41
+
42
+ type DrawerPayload = {
43
+ title: string;
44
+ value: Record<string, unknown>;
45
+ } | null;
46
+
47
+ const joinClassNames = (
48
+ ...classNames: Array<string | false | null | undefined>
49
+ ) => classNames.filter(Boolean).join(' ');
50
+
51
+ const getColumnHeaderTitle = (
52
+ column: string,
53
+ meta?: {
54
+ type?: string | null;
55
+ isPrimaryKey?: boolean;
56
+ isForeignKey?: boolean;
57
+ },
58
+ ) => {
59
+ const details = [
60
+ meta?.type,
61
+ meta?.isPrimaryKey ? 'PK' : null,
62
+ !meta?.isPrimaryKey && meta?.isForeignKey ? 'FK' : null,
63
+ ].filter(Boolean);
64
+
65
+ return details.length > 0 ? `${column} (${details.join(', ')})` : column;
66
+ };
67
+
68
+ export const QueryResultTable = ({
69
+ tableId,
70
+ result,
71
+ columnOrder,
72
+ onColumnOrderChange,
73
+ emptyTitle,
74
+ emptyDescription,
75
+ loading = false,
76
+ showMetadata = true,
77
+ tableClassName,
78
+ shellClassName,
79
+ scrollContainerClassName,
80
+ rowNumberOffset = 0,
81
+ columnMeta,
82
+ hiddenColumnIds = [],
83
+ rowActions,
84
+ }: QueryResultTableProps) => {
85
+ const [drawerPayload, setDrawerPayload] = useState<DrawerPayload>(null);
86
+
87
+ const columns = useMemo(() => result?.columns ?? [], [result]);
88
+ const rows = result?.rows ?? [];
89
+ const metadata = result?.metadata ?? null;
90
+ const visibleColumns = useMemo(
91
+ () => columns.filter((column) => !hiddenColumnIds.includes(column)),
92
+ [columns, hiddenColumnIds],
93
+ );
94
+
95
+ const handleInspectRow = (row: Record<string, unknown>, rowIndex: number) => {
96
+ setDrawerPayload({
97
+ title: `Row ${rowNumberOffset + rowIndex + 1}`,
98
+ value: Object.fromEntries(
99
+ visibleColumns.map((column) => [column, row[column]]),
100
+ ),
101
+ });
102
+ };
103
+
104
+ const tableColumns = useMemo<ColumnDef<Record<string, unknown>, unknown>[]>(
105
+ () => [
106
+ ...visibleColumns.map((column) => ({
107
+ id: column,
108
+ header: () => (
109
+ <span title={getColumnHeaderTitle(column, columnMeta?.[column])}>
110
+ {column}
111
+ </span>
112
+ ),
113
+ accessorFn: (row: Record<string, unknown>) => row[column],
114
+ cell: ({ row }: CellContext<Record<string, unknown>, unknown>) => {
115
+ const value = row.original[column];
116
+
117
+ return (
118
+ <div className="sqlite-cell-value">
119
+ <span className="sqlite-cell-preview">
120
+ {getValuePreview(value)}
121
+ </span>
122
+ <span className="sqlite-cell-kind">{getValueKind(value)}</span>
123
+ </div>
124
+ );
125
+ },
126
+ })),
127
+ ...(rowActions
128
+ ? [
129
+ {
130
+ id: rowActions.columnId,
131
+ header: rowActions.header,
132
+ enableResizing: false,
133
+ size: 112,
134
+ minSize: 112,
135
+ maxSize: 140,
136
+ cell: ({ row }) => rowActions.cell(row.original, row.index),
137
+ } satisfies ColumnDef<Record<string, unknown>, unknown>,
138
+ ]
139
+ : []),
140
+ ],
141
+ [columnMeta, rowActions, visibleColumns],
142
+ );
143
+
144
+ return (
145
+ <>
146
+ {showMetadata && metadata ? (
147
+ <div className="sqlite-inline-metadata">
148
+ <span
149
+ className={joinClassNames(
150
+ 'sqlite-badge',
151
+ getMetadataBadgeClassName(metadata),
152
+ )}
153
+ >
154
+ {metadata.statementType}
155
+ </span>
156
+ <span className="sqlite-inline-stat sqlite-tabular">
157
+ {formatNumber(metadata.rowCount)} rows
158
+ </span>
159
+ <span className="sqlite-inline-stat sqlite-tabular">
160
+ {formatNumber(metadata.changes)} changes
161
+ </span>
162
+ <span className="sqlite-inline-stat sqlite-tabular">
163
+ last insert {formatNumber(metadata.lastInsertRowId)}
164
+ </span>
165
+ <span className="sqlite-inline-stat sqlite-tabular">
166
+ {formatDuration(metadata.durationMs)}
167
+ </span>
168
+ </div>
169
+ ) : null}
170
+
171
+ <SqliteDataTable
172
+ tableId={tableId}
173
+ data={rows}
174
+ columns={tableColumns}
175
+ columnOrder={columnOrder}
176
+ onColumnOrderChange={onColumnOrderChange}
177
+ loading={loading}
178
+ emptyTitle={emptyTitle}
179
+ emptyDescription={emptyDescription}
180
+ shellClassName={shellClassName}
181
+ scrollContainerClassName={scrollContainerClassName}
182
+ tableClassName={tableClassName}
183
+ showRowNumbers
184
+ rowNumberOffset={rowNumberOffset}
185
+ onRowClick={handleInspectRow}
186
+ getRowAriaLabel={(_, rowIndex) =>
187
+ `Inspect row ${rowNumberOffset + rowIndex + 1}`
188
+ }
189
+ />
190
+
191
+ <CellDetailDrawer
192
+ isOpen={!!drawerPayload}
193
+ onClose={() => setDrawerPayload(null)}
194
+ title={drawerPayload?.title ?? 'Row'}
195
+ value={drawerPayload?.value}
196
+ />
197
+ </>
198
+ );
199
+ };
@@ -0,0 +1,352 @@
1
+ import type { Completion } from '@codemirror/autocomplete';
2
+ import type { SQLNamespace } from '@codemirror/lang-sql';
3
+ import { format } from 'sql-formatter';
4
+ import { quoteSqlIdentifier } from '../shared/sql';
5
+ import type {
6
+ SqliteColumnInfo,
7
+ SqliteEntity,
8
+ SqliteSchema,
9
+ } from './sqlite-introspection';
10
+
11
+ export type SqlEditorColumnCacheState = {
12
+ databaseId: string | null;
13
+ entries: Record<string, SqliteColumnInfo[]>;
14
+ };
15
+
16
+ export type SqlEditorColumnCompletionRequest = {
17
+ schemaName: string | null;
18
+ entityName: string;
19
+ from: number;
20
+ to: number;
21
+ };
22
+
23
+ type SqlEditorAliasLookup = Record<
24
+ string,
25
+ {
26
+ schemaName: string | null;
27
+ entityName: string;
28
+ }
29
+ >;
30
+
31
+ const SQL_IDENTIFIER_PATTERN =
32
+ '"(?:[^"]|"")+"|`(?:[^`]|``)+`|\\[[^\\]]+\\]|[A-Za-z_][\\w$]*';
33
+
34
+ const bareIdentifierPattern = /^[A-Za-z_][\w$]*$/;
35
+ const trailingIdentifierPattern = /[A-Za-z_][\w$]*$/;
36
+ const entityMemberPattern = new RegExp(
37
+ `(${SQL_IDENTIFIER_PATTERN})\\s*\\.\\s*(${SQL_IDENTIFIER_PATTERN})\\s*\\.\\s*$`,
38
+ );
39
+ const singleMemberPattern = new RegExp(
40
+ `(${SQL_IDENTIFIER_PATTERN})\\s*\\.\\s*$`,
41
+ );
42
+ const aliasPattern = new RegExp(
43
+ `\\b(?:FROM|JOIN|UPDATE|INTO)\\s+(?:(?:(${SQL_IDENTIFIER_PATTERN})\\s*\\.\\s*)?(${SQL_IDENTIFIER_PATTERN}))(?:\\s+(?:AS\\s+)?(${SQL_IDENTIFIER_PATTERN}))?`,
44
+ 'gi',
45
+ );
46
+
47
+ const normalizeIdentifier = (identifier: string) => identifier.toLowerCase();
48
+
49
+ const unquoteIdentifier = (identifier: string) => {
50
+ if (identifier.startsWith('"') && identifier.endsWith('"')) {
51
+ return identifier.slice(1, -1).replace(/""/g, '"');
52
+ }
53
+
54
+ if (identifier.startsWith('`') && identifier.endsWith('`')) {
55
+ return identifier.slice(1, -1).replace(/``/g, '`');
56
+ }
57
+
58
+ if (identifier.startsWith('[') && identifier.endsWith(']')) {
59
+ return identifier.slice(1, -1).replace(/]]/g, ']');
60
+ }
61
+
62
+ return identifier;
63
+ };
64
+
65
+ const getIdentifierInsertText = (identifier: string) =>
66
+ bareIdentifierPattern.test(identifier)
67
+ ? identifier
68
+ : quoteSqlIdentifier(identifier);
69
+
70
+ const getColumnDetail = (column: SqliteColumnInfo) => {
71
+ const parts = [column.type].filter(Boolean);
72
+
73
+ if (column.primaryKeyOrder > 0) {
74
+ parts.push('PK');
75
+ }
76
+
77
+ if (column.notNull) {
78
+ parts.push('NOT NULL');
79
+ }
80
+
81
+ if (column.hidden > 0) {
82
+ parts.push('HIDDEN');
83
+ }
84
+
85
+ return parts.join(' · ');
86
+ };
87
+
88
+ export const createSqlEditorColumnCache = (
89
+ databaseId: string | null = null,
90
+ ): SqlEditorColumnCacheState => ({
91
+ databaseId,
92
+ entries: {},
93
+ });
94
+
95
+ export const syncSqlEditorColumnCacheDatabase = (
96
+ state: SqlEditorColumnCacheState,
97
+ databaseId: string | null,
98
+ ) =>
99
+ state.databaseId === databaseId
100
+ ? state
101
+ : createSqlEditorColumnCache(databaseId);
102
+
103
+ export const getSqlEditorColumnCacheKey = (
104
+ databaseId: string,
105
+ schemaName: string,
106
+ entityName: string,
107
+ ) => JSON.stringify([databaseId, schemaName, entityName]);
108
+
109
+ export const getSqlEditorCachedColumns = (
110
+ state: SqlEditorColumnCacheState,
111
+ databaseId: string,
112
+ schemaName: string,
113
+ entityName: string,
114
+ ) =>
115
+ state.entries[getSqlEditorColumnCacheKey(databaseId, schemaName, entityName)];
116
+
117
+ export const setSqlEditorCachedColumns = (
118
+ state: SqlEditorColumnCacheState,
119
+ databaseId: string,
120
+ schemaName: string,
121
+ entityName: string,
122
+ columns: SqliteColumnInfo[],
123
+ ): SqlEditorColumnCacheState => {
124
+ const nextState = syncSqlEditorColumnCacheDatabase(state, databaseId);
125
+
126
+ return {
127
+ databaseId: nextState.databaseId,
128
+ entries: {
129
+ ...nextState.entries,
130
+ [getSqlEditorColumnCacheKey(databaseId, schemaName, entityName)]: columns,
131
+ },
132
+ };
133
+ };
134
+
135
+ export const formatSqlScript = (value: string) => {
136
+ const trimmed = value.trim();
137
+
138
+ if (!trimmed) {
139
+ return '';
140
+ }
141
+
142
+ return format(trimmed, {
143
+ language: 'sqlite',
144
+ keywordCase: 'upper',
145
+ linesBetweenQueries: 1,
146
+ tabWidth: 2,
147
+ }).trim();
148
+ };
149
+
150
+ export const buildSqlCompletionSchema = ({
151
+ databaseId,
152
+ schemas,
153
+ entities,
154
+ columnCache,
155
+ }: {
156
+ databaseId: string | null;
157
+ schemas: SqliteSchema[];
158
+ entities: SqliteEntity[];
159
+ columnCache: SqlEditorColumnCacheState;
160
+ }): SQLNamespace => {
161
+ const groupedEntities = new Map<string, SqliteEntity[]>();
162
+
163
+ for (const entity of entities) {
164
+ const schemaEntities = groupedEntities.get(entity.schemaName) ?? [];
165
+ schemaEntities.push(entity);
166
+ groupedEntities.set(entity.schemaName, schemaEntities);
167
+ }
168
+
169
+ const namespace: Record<string, SQLNamespace> = {};
170
+
171
+ for (const schema of schemas) {
172
+ const schemaChildren: Record<string, SQLNamespace> = {};
173
+ const schemaEntities = groupedEntities.get(schema.name) ?? [];
174
+
175
+ for (const entity of schemaEntities) {
176
+ const columns =
177
+ databaseId == null
178
+ ? []
179
+ : (getSqlEditorCachedColumns(
180
+ columnCache,
181
+ databaseId,
182
+ entity.schemaName,
183
+ entity.name,
184
+ ) ?? []);
185
+
186
+ schemaChildren[entity.name] = {
187
+ self: {
188
+ label: entity.name,
189
+ apply: getIdentifierInsertText(entity.name),
190
+ detail: entity.type === 'view' ? 'view' : 'table',
191
+ boost: entity.type === 'view' ? 90 : 100,
192
+ type: entity.type === 'view' ? 'type' : 'class',
193
+ },
194
+ children: columns.map((column) => ({
195
+ label: column.name,
196
+ apply: getIdentifierInsertText(column.name),
197
+ detail: getColumnDetail(column) || undefined,
198
+ boost: column.primaryKeyOrder > 0 ? 80 : 70,
199
+ type: 'property',
200
+ })),
201
+ };
202
+ }
203
+
204
+ namespace[schema.name] = {
205
+ self: {
206
+ label: schema.name,
207
+ apply: getIdentifierInsertText(schema.name),
208
+ detail: 'schema',
209
+ boost: 10,
210
+ type: 'namespace',
211
+ },
212
+ children: schemaChildren,
213
+ };
214
+ }
215
+
216
+ return namespace;
217
+ };
218
+
219
+ export const getDefaultSqlCompletionSchema = (schemas: SqliteSchema[]) =>
220
+ schemas.find((schema) => schema.name === 'main')?.name ?? schemas[0]?.name;
221
+
222
+ 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 => {
236
+ const aliases: SqlEditorAliasLookup = {};
237
+
238
+ let match: RegExpExecArray | null;
239
+ while ((match = aliasPattern.exec(sqlBeforeCursor)) !== null) {
240
+ const schemaName = match[1] ? unquoteIdentifier(match[1]) : null;
241
+ const entityName = unquoteIdentifier(match[2]);
242
+ const alias = match[3] ? unquoteIdentifier(match[3]) : null;
243
+
244
+ if (alias) {
245
+ aliases[normalizeIdentifier(alias)] = { schemaName, entityName };
246
+ }
247
+ }
248
+
249
+ return aliases;
250
+ };
251
+
252
+ export const getSqlEditorColumnCompletionRequest = (
253
+ sql: string,
254
+ cursorPosition: number,
255
+ ): SqlEditorColumnCompletionRequest | null => {
256
+ const beforeCursor = sql.slice(0, cursorPosition);
257
+ const trailingIdentifier = beforeCursor.match(trailingIdentifierPattern)?.[0];
258
+ const replacementLength = trailingIdentifier?.length ?? 0;
259
+ const lookupPrefix = beforeCursor.slice(
260
+ 0,
261
+ beforeCursor.length - replacementLength,
262
+ );
263
+
264
+ const qualifiedMatch = lookupPrefix.match(entityMemberPattern);
265
+ if (qualifiedMatch) {
266
+ return {
267
+ schemaName: unquoteIdentifier(qualifiedMatch[1]),
268
+ entityName: unquoteIdentifier(qualifiedMatch[2]),
269
+ from: cursorPosition - replacementLength,
270
+ to: cursorPosition,
271
+ };
272
+ }
273
+
274
+ const memberMatch = lookupPrefix.match(singleMemberPattern);
275
+ if (memberMatch) {
276
+ return {
277
+ schemaName: null,
278
+ entityName: unquoteIdentifier(memberMatch[1]),
279
+ from: cursorPosition - replacementLength,
280
+ to: cursorPosition,
281
+ };
282
+ }
283
+
284
+ return null;
285
+ };
286
+
287
+ export const resolveSqlEditorEntityReference = ({
288
+ aliases,
289
+ entities,
290
+ request,
291
+ selectedSchemaName,
292
+ }: {
293
+ aliases: SqlEditorAliasLookup;
294
+ entities: SqliteEntity[];
295
+ request: SqlEditorColumnCompletionRequest;
296
+ selectedSchemaName: string | null;
297
+ }): SqliteEntity | null => {
298
+ const findExactEntity = (schemaName: string, entityName: string) =>
299
+ entities.find(
300
+ (entity) =>
301
+ normalizeIdentifier(entity.schemaName) ===
302
+ normalizeIdentifier(schemaName) &&
303
+ normalizeIdentifier(entity.name) === normalizeIdentifier(entityName),
304
+ ) ?? null;
305
+
306
+ if (request.schemaName) {
307
+ return findExactEntity(request.schemaName, request.entityName);
308
+ }
309
+
310
+ const aliasMatch = aliases[normalizeIdentifier(request.entityName)];
311
+ if (aliasMatch) {
312
+ if (aliasMatch.schemaName) {
313
+ return findExactEntity(aliasMatch.schemaName, aliasMatch.entityName);
314
+ }
315
+
316
+ return (
317
+ entities.find(
318
+ (entity) =>
319
+ normalizeIdentifier(entity.name) ===
320
+ normalizeIdentifier(aliasMatch.entityName),
321
+ ) ?? null
322
+ );
323
+ }
324
+
325
+ const entityMatches = entities.filter(
326
+ (entity) =>
327
+ normalizeIdentifier(entity.name) ===
328
+ normalizeIdentifier(request.entityName),
329
+ );
330
+
331
+ if (entityMatches.length === 0) {
332
+ return null;
333
+ }
334
+
335
+ if (selectedSchemaName) {
336
+ const schemaMatch = entityMatches.find(
337
+ (entity) =>
338
+ normalizeIdentifier(entity.schemaName) ===
339
+ normalizeIdentifier(selectedSchemaName),
340
+ );
341
+
342
+ if (schemaMatch) {
343
+ return schemaMatch;
344
+ }
345
+ }
346
+
347
+ return (
348
+ entityMatches.find(
349
+ (entity) => normalizeIdentifier(entity.schemaName) === 'main',
350
+ ) ?? entityMatches[0]
351
+ );
352
+ };