@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.
- package/CHANGELOG.md +8 -0
- package/LICENSE +20 -0
- package/README.md +102 -0
- package/dist/devtools/assets/panel-B3paLkwG.js +82 -0
- package/dist/devtools/assets/panel-CIU0JBOs.css +1 -0
- package/dist/devtools/panel.html +31 -0
- package/dist/react-native/chunks/bridge-values.cjs +5 -0
- package/dist/react-native/chunks/bridge-values.js +258 -0
- package/dist/react-native/chunks/index.require.cjs +1 -0
- package/dist/react-native/chunks/index.require.js +118 -0
- package/dist/react-native/chunks/useRozeniteSqlitePlugin.require.cjs +1 -0
- package/dist/react-native/chunks/useRozeniteSqlitePlugin.require.js +189 -0
- package/dist/react-native/index.cjs +1 -0
- package/dist/react-native/index.d.ts +178 -0
- package/dist/react-native/index.js +16 -0
- package/dist/rozenite.json +1 -0
- package/package.json +83 -0
- package/postcss.config.js +6 -0
- package/react-native.ts +55 -0
- package/rozenite.config.ts +8 -0
- package/src/react-native/adapters/__tests__/expo-sqlite.test.ts +94 -0
- package/src/react-native/adapters/expo-sqlite.ts +230 -0
- package/src/react-native/adapters/generic.ts +88 -0
- package/src/react-native/adapters/index.ts +9 -0
- package/src/react-native/sqlite-view.ts +24 -0
- package/src/react-native/useRozeniteSqlitePlugin.ts +262 -0
- package/src/shared/__tests__/bridge-values.test.ts +34 -0
- package/src/shared/__tests__/sql.test.ts +55 -0
- package/src/shared/bridge-values.ts +170 -0
- package/src/shared/protocol.ts +41 -0
- package/src/shared/sql.ts +420 -0
- package/src/shared/types.ts +81 -0
- package/src/ui/__tests__/sql-editor-utils.test.ts +135 -0
- package/src/ui/__tests__/sqlite-row-edit-value.test.ts +22 -0
- package/src/ui/__tests__/sqlite-row-mutations.test.ts +310 -0
- package/src/ui/__tests__/sqlite-table-column-order.test.ts +83 -0
- package/src/ui/__tests__/value-utils.test.tsx +12 -0
- package/src/ui/cell-detail-drawer.tsx +65 -0
- package/src/ui/globals.css +1415 -0
- package/src/ui/panel.tsx +2815 -0
- package/src/ui/query-result-table.tsx +199 -0
- package/src/ui/sql-editor-utils.ts +352 -0
- package/src/ui/sql-editor.tsx +509 -0
- package/src/ui/sqlite-data-table.tsx +296 -0
- package/src/ui/sqlite-introspection.ts +189 -0
- package/src/ui/sqlite-modal-controls.tsx +32 -0
- package/src/ui/sqlite-row-delete-modal.tsx +130 -0
- package/src/ui/sqlite-row-edit-modal.tsx +487 -0
- package/src/ui/sqlite-row-edit-value.ts +53 -0
- package/src/ui/sqlite-row-mutations.ts +246 -0
- package/src/ui/sqlite-table-column-order.ts +154 -0
- package/src/ui/use-sqlite-requests.ts +205 -0
- package/src/ui/utils.ts +107 -0
- package/src/ui/value-utils.tsx +162 -0
- package/tsconfig.json +36 -0
- package/vite.config.ts +20 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import type { SqliteStatementType } from './types';
|
|
2
|
+
|
|
3
|
+
const isWhitespace = (char: string) => /\s/.test(char);
|
|
4
|
+
|
|
5
|
+
export const countSqlStatements = (sql: string) => {
|
|
6
|
+
let count = 0;
|
|
7
|
+
let hasToken = false;
|
|
8
|
+
let i = 0;
|
|
9
|
+
let mode:
|
|
10
|
+
| 'single-quote'
|
|
11
|
+
| 'double-quote'
|
|
12
|
+
| 'backtick'
|
|
13
|
+
| 'bracket'
|
|
14
|
+
| 'line-comment'
|
|
15
|
+
| 'block-comment'
|
|
16
|
+
| null = null;
|
|
17
|
+
|
|
18
|
+
while (i < sql.length) {
|
|
19
|
+
const char = sql[i];
|
|
20
|
+
const next = sql[i + 1];
|
|
21
|
+
|
|
22
|
+
if (mode === 'line-comment') {
|
|
23
|
+
if (char === '\n') {
|
|
24
|
+
mode = null;
|
|
25
|
+
}
|
|
26
|
+
i += 1;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (mode === 'block-comment') {
|
|
31
|
+
if (char === '*' && next === '/') {
|
|
32
|
+
mode = null;
|
|
33
|
+
i += 2;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
i += 1;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (mode === 'single-quote') {
|
|
41
|
+
if (char === "'" && next === "'") {
|
|
42
|
+
i += 2;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (char === "'") {
|
|
46
|
+
mode = null;
|
|
47
|
+
}
|
|
48
|
+
i += 1;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (mode === 'double-quote') {
|
|
53
|
+
if (char === '"' && next === '"') {
|
|
54
|
+
i += 2;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (char === '"') {
|
|
58
|
+
mode = null;
|
|
59
|
+
}
|
|
60
|
+
i += 1;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (mode === 'backtick') {
|
|
65
|
+
if (char === '`' && next === '`') {
|
|
66
|
+
i += 2;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (char === '`') {
|
|
70
|
+
mode = null;
|
|
71
|
+
}
|
|
72
|
+
i += 1;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (mode === 'bracket') {
|
|
77
|
+
if (char === ']' && next === ']') {
|
|
78
|
+
i += 2;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (char === ']') {
|
|
82
|
+
mode = null;
|
|
83
|
+
}
|
|
84
|
+
i += 1;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (char === '-' && next === '-') {
|
|
89
|
+
mode = 'line-comment';
|
|
90
|
+
i += 2;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (char === '/' && next === '*') {
|
|
95
|
+
mode = 'block-comment';
|
|
96
|
+
i += 2;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (char === "'") {
|
|
101
|
+
mode = 'single-quote';
|
|
102
|
+
hasToken = true;
|
|
103
|
+
i += 1;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (char === '"') {
|
|
108
|
+
mode = 'double-quote';
|
|
109
|
+
hasToken = true;
|
|
110
|
+
i += 1;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (char === '`') {
|
|
115
|
+
mode = 'backtick';
|
|
116
|
+
hasToken = true;
|
|
117
|
+
i += 1;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (char === '[') {
|
|
122
|
+
mode = 'bracket';
|
|
123
|
+
hasToken = true;
|
|
124
|
+
i += 1;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (char === ';') {
|
|
129
|
+
if (hasToken) {
|
|
130
|
+
count += 1;
|
|
131
|
+
hasToken = false;
|
|
132
|
+
}
|
|
133
|
+
i += 1;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!isWhitespace(char)) {
|
|
138
|
+
hasToken = true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
i += 1;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (hasToken) {
|
|
145
|
+
count += 1;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return count;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export type SqlStatementSegment = {
|
|
152
|
+
text: string;
|
|
153
|
+
start: number;
|
|
154
|
+
end: number;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export const splitSqlStatements = (sql: string): SqlStatementSegment[] => {
|
|
158
|
+
const segments: SqlStatementSegment[] = [];
|
|
159
|
+
let hasToken = false;
|
|
160
|
+
let segmentStart = 0;
|
|
161
|
+
let i = 0;
|
|
162
|
+
let mode:
|
|
163
|
+
| 'single-quote'
|
|
164
|
+
| 'double-quote'
|
|
165
|
+
| 'backtick'
|
|
166
|
+
| 'bracket'
|
|
167
|
+
| 'line-comment'
|
|
168
|
+
| 'block-comment'
|
|
169
|
+
| null = null;
|
|
170
|
+
|
|
171
|
+
const pushSegment = (end: number) => {
|
|
172
|
+
const text = sql.slice(segmentStart, end).trim();
|
|
173
|
+
|
|
174
|
+
if (text) {
|
|
175
|
+
segments.push({
|
|
176
|
+
text,
|
|
177
|
+
start: segmentStart,
|
|
178
|
+
end,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
segmentStart = end + 1;
|
|
183
|
+
hasToken = false;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
while (i < sql.length) {
|
|
187
|
+
const char = sql[i];
|
|
188
|
+
const next = sql[i + 1];
|
|
189
|
+
|
|
190
|
+
if (mode === 'line-comment') {
|
|
191
|
+
if (char === '\n') {
|
|
192
|
+
mode = null;
|
|
193
|
+
}
|
|
194
|
+
i += 1;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (mode === 'block-comment') {
|
|
199
|
+
if (char === '*' && next === '/') {
|
|
200
|
+
mode = null;
|
|
201
|
+
i += 2;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
i += 1;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (mode === 'single-quote') {
|
|
209
|
+
if (char === "'" && next === "'") {
|
|
210
|
+
i += 2;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (char === "'") {
|
|
214
|
+
mode = null;
|
|
215
|
+
}
|
|
216
|
+
i += 1;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (mode === 'double-quote') {
|
|
221
|
+
if (char === '"' && next === '"') {
|
|
222
|
+
i += 2;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (char === '"') {
|
|
226
|
+
mode = null;
|
|
227
|
+
}
|
|
228
|
+
i += 1;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (mode === 'backtick') {
|
|
233
|
+
if (char === '`' && next === '`') {
|
|
234
|
+
i += 2;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (char === '`') {
|
|
238
|
+
mode = null;
|
|
239
|
+
}
|
|
240
|
+
i += 1;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (mode === 'bracket') {
|
|
245
|
+
if (char === ']' && next === ']') {
|
|
246
|
+
i += 2;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (char === ']') {
|
|
250
|
+
mode = null;
|
|
251
|
+
}
|
|
252
|
+
i += 1;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (char === '-' && next === '-') {
|
|
257
|
+
mode = 'line-comment';
|
|
258
|
+
i += 2;
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (char === '/' && next === '*') {
|
|
263
|
+
mode = 'block-comment';
|
|
264
|
+
i += 2;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (char === "'") {
|
|
269
|
+
mode = 'single-quote';
|
|
270
|
+
hasToken = true;
|
|
271
|
+
i += 1;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (char === '"') {
|
|
276
|
+
mode = 'double-quote';
|
|
277
|
+
hasToken = true;
|
|
278
|
+
i += 1;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (char === '`') {
|
|
283
|
+
mode = 'backtick';
|
|
284
|
+
hasToken = true;
|
|
285
|
+
i += 1;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (char === '[') {
|
|
290
|
+
mode = 'bracket';
|
|
291
|
+
hasToken = true;
|
|
292
|
+
i += 1;
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (char === ';') {
|
|
297
|
+
if (hasToken) {
|
|
298
|
+
pushSegment(i);
|
|
299
|
+
} else {
|
|
300
|
+
segmentStart = i + 1;
|
|
301
|
+
}
|
|
302
|
+
i += 1;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (!isWhitespace(char)) {
|
|
307
|
+
hasToken = true;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
i += 1;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (hasToken) {
|
|
314
|
+
pushSegment(sql.length);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return segments;
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
export const getStatementAtCursor = (sql: string, cursor: number) => {
|
|
321
|
+
const segments = splitSqlStatements(sql);
|
|
322
|
+
|
|
323
|
+
if (segments.length === 0) {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const match = segments.find(
|
|
328
|
+
(segment) => cursor >= segment.start && cursor <= segment.end + 1,
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
return match ?? segments[0];
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
export const normalizeSingleStatementSql = (sql: string) => {
|
|
335
|
+
const statementCount = countSqlStatements(sql);
|
|
336
|
+
|
|
337
|
+
if (statementCount === 0) {
|
|
338
|
+
throw new Error('Query cannot be empty.');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (statementCount > 1) {
|
|
342
|
+
throw new Error('Only a single SQL statement is supported in v1.');
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return sql.trim().replace(/;\s*$/, '').trim();
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const readLeadingKeyword = (sql: string) => {
|
|
349
|
+
let i = 0;
|
|
350
|
+
|
|
351
|
+
while (i < sql.length) {
|
|
352
|
+
const char = sql[i];
|
|
353
|
+
const next = sql[i + 1];
|
|
354
|
+
|
|
355
|
+
if (isWhitespace(char)) {
|
|
356
|
+
i += 1;
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (char === '-' && next === '-') {
|
|
361
|
+
i += 2;
|
|
362
|
+
while (i < sql.length && sql[i] !== '\n') {
|
|
363
|
+
i += 1;
|
|
364
|
+
}
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (char === '/' && next === '*') {
|
|
369
|
+
i += 2;
|
|
370
|
+
while (i < sql.length && !(sql[i] === '*' && sql[i + 1] === '/')) {
|
|
371
|
+
i += 1;
|
|
372
|
+
}
|
|
373
|
+
i += 2;
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const start = i;
|
|
381
|
+
|
|
382
|
+
while (i < sql.length && /[A-Za-z]/.test(sql[i])) {
|
|
383
|
+
i += 1;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return sql.slice(start, i).toLowerCase();
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
export const classifySqlStatement = (sql: string): SqliteStatementType => {
|
|
390
|
+
const keyword = readLeadingKeyword(sql);
|
|
391
|
+
|
|
392
|
+
if (
|
|
393
|
+
keyword === 'select' ||
|
|
394
|
+
keyword === 'insert' ||
|
|
395
|
+
keyword === 'update' ||
|
|
396
|
+
keyword === 'delete' ||
|
|
397
|
+
keyword === 'pragma' ||
|
|
398
|
+
keyword === 'create' ||
|
|
399
|
+
keyword === 'alter' ||
|
|
400
|
+
keyword === 'drop' ||
|
|
401
|
+
keyword === 'explain' ||
|
|
402
|
+
keyword === 'with'
|
|
403
|
+
) {
|
|
404
|
+
return keyword;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return 'other';
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
export const statementReturnsRows = (statementType: SqliteStatementType) =>
|
|
411
|
+
statementType === 'select' ||
|
|
412
|
+
statementType === 'pragma' ||
|
|
413
|
+
statementType === 'explain' ||
|
|
414
|
+
statementType === 'with';
|
|
415
|
+
|
|
416
|
+
export const quoteSqlIdentifier = (identifier: string) =>
|
|
417
|
+
`"${identifier.replace(/"/g, '""')}"`;
|
|
418
|
+
|
|
419
|
+
export const escapeSqlString = (value: string) =>
|
|
420
|
+
`'${value.replace(/'/g, "''")}'`;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export type SqliteStatementType =
|
|
2
|
+
| 'select'
|
|
3
|
+
| 'insert'
|
|
4
|
+
| 'update'
|
|
5
|
+
| 'delete'
|
|
6
|
+
| 'pragma'
|
|
7
|
+
| 'create'
|
|
8
|
+
| 'alter'
|
|
9
|
+
| 'drop'
|
|
10
|
+
| 'explain'
|
|
11
|
+
| 'with'
|
|
12
|
+
| 'other';
|
|
13
|
+
|
|
14
|
+
export type SqliteQueryParams = unknown[] | Record<string, unknown>;
|
|
15
|
+
|
|
16
|
+
export type SqliteStatementInput = {
|
|
17
|
+
sql: string;
|
|
18
|
+
params?: SqliteQueryParams;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type SqliteQueryMetadata = {
|
|
22
|
+
statementType: SqliteStatementType;
|
|
23
|
+
rowCount: number;
|
|
24
|
+
changes: number | null;
|
|
25
|
+
lastInsertRowId: number | null;
|
|
26
|
+
durationMs: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type SqliteQueryResult = {
|
|
30
|
+
rows: Record<string, unknown>[];
|
|
31
|
+
columns: string[];
|
|
32
|
+
metadata: SqliteQueryMetadata;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type SqliteStatementExecutionResult = {
|
|
36
|
+
input: SqliteStatementInput;
|
|
37
|
+
result: SqliteQueryResult;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type SqliteScriptStatementResult = {
|
|
41
|
+
index: number;
|
|
42
|
+
start: number;
|
|
43
|
+
end: number;
|
|
44
|
+
input: SqliteStatementInput;
|
|
45
|
+
execution?: SqliteStatementExecutionResult;
|
|
46
|
+
error?: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type SqliteScriptResult = {
|
|
50
|
+
statements: SqliteScriptStatementResult[];
|
|
51
|
+
totalStatementCount: number;
|
|
52
|
+
failedStatementIndex: number | null;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type SqliteExecuteStatementsRunner = (
|
|
56
|
+
statements: SqliteStatementInput[],
|
|
57
|
+
) => Promise<SqliteQueryResult[]>;
|
|
58
|
+
|
|
59
|
+
export type SqliteExecuteStatementsError = Error & {
|
|
60
|
+
completedResults?: SqliteQueryResult[];
|
|
61
|
+
failedStatementIndex?: number;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type SqliteDatabaseNode = {
|
|
65
|
+
id: string;
|
|
66
|
+
name: string;
|
|
67
|
+
executeStatements: SqliteExecuteStatementsRunner;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export type SqliteAdapter = {
|
|
71
|
+
id: string;
|
|
72
|
+
name: string;
|
|
73
|
+
databases: SqliteDatabaseNode[];
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type SqliteDatabaseInfo = {
|
|
77
|
+
id: string;
|
|
78
|
+
name: string;
|
|
79
|
+
adapterId: string;
|
|
80
|
+
adapterName: string;
|
|
81
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type {
|
|
3
|
+
SqliteColumnInfo,
|
|
4
|
+
SqliteEntity,
|
|
5
|
+
SqliteSchema,
|
|
6
|
+
} from '../sqlite-introspection';
|
|
7
|
+
import {
|
|
8
|
+
buildSqlCompletionSchema,
|
|
9
|
+
createSqlEditorColumnCache,
|
|
10
|
+
extractSqlEditorAliases,
|
|
11
|
+
formatSqlScript,
|
|
12
|
+
getSqlEditorColumnCompletionRequest,
|
|
13
|
+
resolveSqlEditorEntityReference,
|
|
14
|
+
setSqlEditorCachedColumns,
|
|
15
|
+
syncSqlEditorColumnCacheDatabase,
|
|
16
|
+
} from '../sql-editor-utils';
|
|
17
|
+
|
|
18
|
+
const schemas: SqliteSchema[] = [
|
|
19
|
+
{
|
|
20
|
+
seq: 0,
|
|
21
|
+
name: 'main',
|
|
22
|
+
file: null,
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const entities: SqliteEntity[] = [
|
|
27
|
+
{
|
|
28
|
+
schemaName: 'main',
|
|
29
|
+
name: 'projects',
|
|
30
|
+
type: 'table',
|
|
31
|
+
sql: 'CREATE TABLE projects(id INTEGER PRIMARY KEY, name TEXT)',
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const columns: SqliteColumnInfo[] = [
|
|
36
|
+
{
|
|
37
|
+
cid: 0,
|
|
38
|
+
name: 'id',
|
|
39
|
+
type: 'INTEGER',
|
|
40
|
+
notNull: true,
|
|
41
|
+
defaultValue: null,
|
|
42
|
+
primaryKeyOrder: 1,
|
|
43
|
+
hidden: 0,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
cid: 1,
|
|
47
|
+
name: 'name',
|
|
48
|
+
type: 'TEXT',
|
|
49
|
+
notNull: false,
|
|
50
|
+
defaultValue: null,
|
|
51
|
+
primaryKeyOrder: 0,
|
|
52
|
+
hidden: 0,
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
describe('sql editor utilities', () => {
|
|
57
|
+
it('formats SQLite SQL with uppercase keywords and multiline layout', () => {
|
|
58
|
+
const formatted = formatSqlScript(
|
|
59
|
+
'select id, name from projects where archived = 0 order by name',
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
expect(formatted).toContain('SELECT');
|
|
63
|
+
expect(formatted).toContain('\nFROM\n projects');
|
|
64
|
+
expect(formatted).toContain('\nWHERE\n archived = 0');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('builds a completion schema from cached table columns', () => {
|
|
68
|
+
const cache = setSqlEditorCachedColumns(
|
|
69
|
+
createSqlEditorColumnCache('db-1'),
|
|
70
|
+
'db-1',
|
|
71
|
+
'main',
|
|
72
|
+
'projects',
|
|
73
|
+
columns,
|
|
74
|
+
);
|
|
75
|
+
const namespace = buildSqlCompletionSchema({
|
|
76
|
+
columnCache: cache,
|
|
77
|
+
databaseId: 'db-1',
|
|
78
|
+
entities,
|
|
79
|
+
schemas,
|
|
80
|
+
}) as unknown as Record<
|
|
81
|
+
string,
|
|
82
|
+
{
|
|
83
|
+
children: Record<
|
|
84
|
+
string,
|
|
85
|
+
{
|
|
86
|
+
children: Array<{ label: string }>;
|
|
87
|
+
self: { detail: string };
|
|
88
|
+
}
|
|
89
|
+
>;
|
|
90
|
+
}
|
|
91
|
+
>;
|
|
92
|
+
|
|
93
|
+
expect(namespace.main.children.projects.self.detail).toBe('table');
|
|
94
|
+
expect(namespace.main.children.projects.children).toEqual([
|
|
95
|
+
expect.objectContaining({ label: 'id' }),
|
|
96
|
+
expect.objectContaining({ label: 'name' }),
|
|
97
|
+
]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('resets cached columns when the selected database changes', () => {
|
|
101
|
+
const initialCache = setSqlEditorCachedColumns(
|
|
102
|
+
createSqlEditorColumnCache('db-1'),
|
|
103
|
+
'db-1',
|
|
104
|
+
'main',
|
|
105
|
+
'projects',
|
|
106
|
+
columns,
|
|
107
|
+
);
|
|
108
|
+
const nextCache = syncSqlEditorColumnCacheDatabase(initialCache, 'db-2');
|
|
109
|
+
|
|
110
|
+
expect(nextCache.databaseId).toBe('db-2');
|
|
111
|
+
expect(nextCache.entries).toEqual({});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('resolves aliased table references for column completion', () => {
|
|
115
|
+
const sql = 'SELECT * FROM main.projects AS p WHERE p.';
|
|
116
|
+
const cursorPosition = sql.length;
|
|
117
|
+
const request = getSqlEditorColumnCompletionRequest(sql, cursorPosition);
|
|
118
|
+
|
|
119
|
+
expect(request).toEqual({
|
|
120
|
+
schemaName: null,
|
|
121
|
+
entityName: 'p',
|
|
122
|
+
from: cursorPosition,
|
|
123
|
+
to: cursorPosition,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const entity = resolveSqlEditorEntityReference({
|
|
127
|
+
aliases: extractSqlEditorAliases(sql),
|
|
128
|
+
entities,
|
|
129
|
+
request: request!,
|
|
130
|
+
selectedSchemaName: 'main',
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(entity).toEqual(entities[0]);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { parseEditableFieldValue } from '../sqlite-row-edit-value';
|
|
3
|
+
|
|
4
|
+
describe('sqlite row edit value helpers', () => {
|
|
5
|
+
it('converts blob-ish drafts into Uint8Array bind values', () => {
|
|
6
|
+
expect(
|
|
7
|
+
parseEditableFieldValue({
|
|
8
|
+
kind: 'blob-ish',
|
|
9
|
+
rawValue: '[1, 2, 255]',
|
|
10
|
+
}),
|
|
11
|
+
).toEqual(new Uint8Array([1, 2, 255]));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('serializes json drafts into JSON text bind values', () => {
|
|
15
|
+
expect(
|
|
16
|
+
parseEditableFieldValue({
|
|
17
|
+
kind: 'json',
|
|
18
|
+
rawValue: '{\n "ok": true,\n "count": 2\n}',
|
|
19
|
+
}),
|
|
20
|
+
).toBe('{"ok":true,"count":2}');
|
|
21
|
+
});
|
|
22
|
+
});
|