sqlite-hub 1.4.0 → 1.5.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/README.md +9 -0
- package/bin/sqlite-hub.js +555 -122
- package/docs/API.md +30 -0
- package/docs/CLI.md +22 -0
- package/docs/CLI_API_PARITY.md +61 -0
- package/docs/changelog.md +9 -1
- package/docs/guidelines/AGENTS.md +32 -0
- package/docs/{DESIGN_GUIDELINES.md → guidelines/DESIGN.md} +10 -0
- package/docs/todo.md +1 -13
- package/frontend/js/api.js +45 -0
- package/frontend/js/app.js +192 -2
- package/frontend/js/components/connectionCard.js +3 -1
- package/frontend/js/components/modal.js +356 -15
- package/frontend/js/components/sidebar.js +41 -7
- package/frontend/js/components/topNav.js +2 -14
- package/frontend/js/router.js +10 -0
- package/frontend/js/store.js +448 -0
- package/frontend/js/utils/inputClear.js +36 -0
- package/frontend/js/utils/syntheticData.js +240 -0
- package/frontend/js/views/backups.js +52 -0
- package/frontend/js/views/data.js +16 -0
- package/frontend/js/views/logs.js +339 -0
- package/frontend/js/views/settings.js +77 -27
- package/frontend/js/views/structure.js +6 -0
- package/frontend/js/views/tableAdvisor.js +385 -0
- package/frontend/js/views/tableDesigner.js +6 -0
- package/frontend/styles/components.css +149 -0
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/views.css +8 -0
- package/package.json +1 -1
- package/server/routes/data.js +45 -0
- package/server/routes/externalApi.js +285 -2
- package/server/routes/logs.js +127 -0
- package/server/server.js +3 -0
- package/server/services/databaseCommandService.js +45 -0
- package/server/services/sqlite/dataBrowserService.js +36 -0
- package/server/services/sqlite/introspection.js +226 -22
- package/server/services/sqlite/syntheticDataGenerator.js +728 -0
- package/server/services/sqlite/tableAdvisor.js +790 -0
- package/server/services/storage/appStateStore.js +773 -169
|
@@ -160,35 +160,66 @@ function normalizeIdentifier(value) {
|
|
|
160
160
|
return text.toLowerCase();
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
function
|
|
163
|
+
function parseSqlStringLiteral(text = "", startIndex = 0) {
|
|
164
|
+
let value = "";
|
|
165
|
+
let index = startIndex + 1;
|
|
166
|
+
|
|
167
|
+
while (index < text.length) {
|
|
168
|
+
if (text[index] === "'") {
|
|
169
|
+
if (text[index + 1] === "'") {
|
|
170
|
+
value += "'";
|
|
171
|
+
index += 2;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
value,
|
|
177
|
+
nextIndex: index + 1,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
value += text[index];
|
|
182
|
+
index += 1;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function parseSqlSimpleListValues(text = "") {
|
|
164
189
|
const values = [];
|
|
165
190
|
let index = 0;
|
|
166
191
|
|
|
167
192
|
while (index < text.length) {
|
|
168
|
-
|
|
193
|
+
const character = text[index];
|
|
194
|
+
|
|
195
|
+
if (/\s|,/.test(character)) {
|
|
169
196
|
index += 1;
|
|
170
197
|
continue;
|
|
171
198
|
}
|
|
172
199
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
while (index < text.length) {
|
|
177
|
-
if (text[index] === "'") {
|
|
178
|
-
if (text[index + 1] === "'") {
|
|
179
|
-
value += "'";
|
|
180
|
-
index += 2;
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
200
|
+
if (character === "'") {
|
|
201
|
+
const parsed = parseSqlStringLiteral(text, index);
|
|
183
202
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
break;
|
|
203
|
+
if (!parsed) {
|
|
204
|
+
return [];
|
|
187
205
|
}
|
|
188
206
|
|
|
189
|
-
value
|
|
190
|
-
index
|
|
207
|
+
values.push(parsed.value);
|
|
208
|
+
index = parsed.nextIndex;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const commaIndex = text.indexOf(",", index);
|
|
213
|
+
const endIndex = commaIndex === -1 ? text.length : commaIndex;
|
|
214
|
+
const token = text.slice(index, endIndex).trim();
|
|
215
|
+
const number = parseSqlNumberToken(token);
|
|
216
|
+
|
|
217
|
+
if (number === null) {
|
|
218
|
+
return [];
|
|
191
219
|
}
|
|
220
|
+
|
|
221
|
+
values.push(number);
|
|
222
|
+
index = endIndex;
|
|
192
223
|
}
|
|
193
224
|
|
|
194
225
|
return values;
|
|
@@ -219,7 +250,7 @@ function findColumnInListExpression(expression, columnName) {
|
|
|
219
250
|
continue;
|
|
220
251
|
}
|
|
221
252
|
|
|
222
|
-
const values =
|
|
253
|
+
const values = parseSqlSimpleListValues(expression.slice(openIndex + 1, closeIndex));
|
|
223
254
|
|
|
224
255
|
if (values.length) {
|
|
225
256
|
return values;
|
|
@@ -229,6 +260,137 @@ function findColumnInListExpression(expression, columnName) {
|
|
|
229
260
|
return [];
|
|
230
261
|
}
|
|
231
262
|
|
|
263
|
+
const SQL_IDENTIFIER_SOURCE =
|
|
264
|
+
'(?:"(?:[^"]|"")+"|`(?:[^`]|``)+`|\\[[^\\]]+\\]|[A-Za-z_][A-Za-z0-9_$]*)';
|
|
265
|
+
const SQL_NUMBER_SOURCE = "[+-]?(?:\\d+(?:\\.\\d+)?|\\.\\d+)(?:e[+-]?\\d+)?";
|
|
266
|
+
const SQL_NUMBER_EXACT_PATTERN = new RegExp(`^${SQL_NUMBER_SOURCE}$`, "i");
|
|
267
|
+
|
|
268
|
+
function parseSqlNumberToken(value) {
|
|
269
|
+
const text = String(value ?? "").trim();
|
|
270
|
+
|
|
271
|
+
if (!SQL_NUMBER_EXACT_PATTERN.test(text)) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const number = Number(text);
|
|
276
|
+
|
|
277
|
+
return Number.isFinite(number) ? number : null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function tokenMatchesColumn(token, columnName) {
|
|
281
|
+
return normalizeIdentifier(token) === normalizeIdentifier(columnName);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function applyIntegerLowerBound(range, value, inclusive) {
|
|
285
|
+
const min = inclusive ? Math.ceil(value) : Math.floor(value) + 1;
|
|
286
|
+
|
|
287
|
+
range.min = range.min === null ? min : Math.max(range.min, min);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function applyIntegerUpperBound(range, value, inclusive) {
|
|
291
|
+
const max = inclusive ? Math.floor(value) : Math.ceil(value) - 1;
|
|
292
|
+
|
|
293
|
+
range.max = range.max === null ? max : Math.min(range.max, max);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function applyIntegerComparison(range, operator, value) {
|
|
297
|
+
switch (operator) {
|
|
298
|
+
case ">":
|
|
299
|
+
applyIntegerLowerBound(range, value, false);
|
|
300
|
+
break;
|
|
301
|
+
case ">=":
|
|
302
|
+
applyIntegerLowerBound(range, value, true);
|
|
303
|
+
break;
|
|
304
|
+
case "<":
|
|
305
|
+
applyIntegerUpperBound(range, value, false);
|
|
306
|
+
break;
|
|
307
|
+
case "<=":
|
|
308
|
+
applyIntegerUpperBound(range, value, true);
|
|
309
|
+
break;
|
|
310
|
+
case "=":
|
|
311
|
+
applyIntegerLowerBound(range, value, true);
|
|
312
|
+
applyIntegerUpperBound(range, value, true);
|
|
313
|
+
break;
|
|
314
|
+
default:
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function reverseComparisonOperator(operator) {
|
|
320
|
+
return {
|
|
321
|
+
">": "<",
|
|
322
|
+
">=": "<=",
|
|
323
|
+
"<": ">",
|
|
324
|
+
"<=": ">=",
|
|
325
|
+
"=": "=",
|
|
326
|
+
}[operator];
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function findColumnIntegerRangeInExpression(expression, columnName) {
|
|
330
|
+
const range = { min: null, max: null };
|
|
331
|
+
let found = false;
|
|
332
|
+
const betweenPattern = new RegExp(
|
|
333
|
+
`(${SQL_IDENTIFIER_SOURCE})\\s+BETWEEN\\s+(${SQL_NUMBER_SOURCE})\\s+AND\\s+(${SQL_NUMBER_SOURCE})`,
|
|
334
|
+
"gi"
|
|
335
|
+
);
|
|
336
|
+
const comparisonPattern = new RegExp(
|
|
337
|
+
`(${SQL_IDENTIFIER_SOURCE}|${SQL_NUMBER_SOURCE})\\s*(<=|>=|=|<|>)\\s*(${SQL_IDENTIFIER_SOURCE}|${SQL_NUMBER_SOURCE})`,
|
|
338
|
+
"gi"
|
|
339
|
+
);
|
|
340
|
+
let match;
|
|
341
|
+
|
|
342
|
+
while ((match = betweenPattern.exec(expression))) {
|
|
343
|
+
const identifier = match[1];
|
|
344
|
+
const min = parseSqlNumberToken(match[2]);
|
|
345
|
+
const max = parseSqlNumberToken(match[3]);
|
|
346
|
+
|
|
347
|
+
if (!tokenMatchesColumn(identifier, columnName) || min === null || max === null) {
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
applyIntegerLowerBound(range, min, true);
|
|
352
|
+
applyIntegerUpperBound(range, max, true);
|
|
353
|
+
found = true;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
while ((match = comparisonPattern.exec(expression))) {
|
|
357
|
+
const left = match[1];
|
|
358
|
+
const operator = match[2];
|
|
359
|
+
const right = match[3];
|
|
360
|
+
const leftIsColumn = tokenMatchesColumn(left, columnName);
|
|
361
|
+
const rightIsColumn = tokenMatchesColumn(right, columnName);
|
|
362
|
+
|
|
363
|
+
if (leftIsColumn) {
|
|
364
|
+
const value = parseSqlNumberToken(right);
|
|
365
|
+
|
|
366
|
+
if (value !== null) {
|
|
367
|
+
applyIntegerComparison(range, operator, value);
|
|
368
|
+
found = true;
|
|
369
|
+
}
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (rightIsColumn) {
|
|
374
|
+
const value = parseSqlNumberToken(left);
|
|
375
|
+
const reversedOperator = reverseComparisonOperator(operator);
|
|
376
|
+
|
|
377
|
+
if (value !== null && reversedOperator) {
|
|
378
|
+
applyIntegerComparison(range, reversedOperator, value);
|
|
379
|
+
found = true;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (!found) {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
...(range.min !== null ? { min: range.min } : {}),
|
|
390
|
+
...(range.max !== null ? { max: range.max } : {}),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
232
394
|
function parseCheckAllowedValues(ddl = "", columns = []) {
|
|
233
395
|
const allowedValuesByColumn = new Map();
|
|
234
396
|
const expressions = extractCheckExpressions(ddl);
|
|
@@ -256,6 +418,42 @@ function parseCheckAllowedValues(ddl = "", columns = []) {
|
|
|
256
418
|
return allowedValuesByColumn;
|
|
257
419
|
}
|
|
258
420
|
|
|
421
|
+
function parseCheckIntegerRanges(ddl = "", columns = []) {
|
|
422
|
+
const integerRangesByColumn = new Map();
|
|
423
|
+
const expressions = extractCheckExpressions(ddl);
|
|
424
|
+
|
|
425
|
+
columns
|
|
426
|
+
.filter((column) => column.affinity === "INTEGER")
|
|
427
|
+
.forEach((column) => {
|
|
428
|
+
const range = { min: null, max: null };
|
|
429
|
+
|
|
430
|
+
expressions.forEach((expression) => {
|
|
431
|
+
const expressionRange = findColumnIntegerRangeInExpression(expression, column.name);
|
|
432
|
+
|
|
433
|
+
if (!expressionRange) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (Number.isSafeInteger(expressionRange.min)) {
|
|
438
|
+
range.min = range.min === null ? expressionRange.min : Math.max(range.min, expressionRange.min);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (Number.isSafeInteger(expressionRange.max)) {
|
|
442
|
+
range.max = range.max === null ? expressionRange.max : Math.min(range.max, expressionRange.max);
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
if (range.min !== null || range.max !== null) {
|
|
447
|
+
integerRangesByColumn.set(column.name, {
|
|
448
|
+
...(range.min !== null ? { min: range.min } : {}),
|
|
449
|
+
...(range.max !== null ? { max: range.max } : {}),
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
return integerRangesByColumn;
|
|
455
|
+
}
|
|
456
|
+
|
|
259
457
|
function groupForeignKeys(rows) {
|
|
260
458
|
const grouped = new Map();
|
|
261
459
|
|
|
@@ -336,11 +534,17 @@ function getTableDetail(db, tableName, options = {}) {
|
|
|
336
534
|
.map((column) => normalizeColumn(column, visibleSet))
|
|
337
535
|
.sort((left, right) => left.cid - right.cid);
|
|
338
536
|
const allowedValuesByColumn = parseCheckAllowedValues(entry.sql, columns);
|
|
537
|
+
const integerRangesByColumn = parseCheckIntegerRanges(entry.sql, columns);
|
|
339
538
|
|
|
340
|
-
columns = columns.map((column) =>
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
539
|
+
columns = columns.map((column) => {
|
|
540
|
+
const integerRange = integerRangesByColumn.get(column.name);
|
|
541
|
+
|
|
542
|
+
return {
|
|
543
|
+
...column,
|
|
544
|
+
allowedValues: allowedValuesByColumn.get(column.name) ?? [],
|
|
545
|
+
...(integerRange ? { integerRange } : {}),
|
|
546
|
+
};
|
|
547
|
+
});
|
|
344
548
|
|
|
345
549
|
const foreignKeys = groupForeignKeys(
|
|
346
550
|
db.prepare(`PRAGMA foreign_key_list(${quoteIdentifier(tableName)})`).all()
|