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.
Files changed (40) hide show
  1. package/README.md +9 -0
  2. package/bin/sqlite-hub.js +555 -122
  3. package/docs/API.md +30 -0
  4. package/docs/CLI.md +22 -0
  5. package/docs/CLI_API_PARITY.md +61 -0
  6. package/docs/changelog.md +9 -1
  7. package/docs/guidelines/AGENTS.md +32 -0
  8. package/docs/{DESIGN_GUIDELINES.md → guidelines/DESIGN.md} +10 -0
  9. package/docs/todo.md +1 -13
  10. package/frontend/js/api.js +45 -0
  11. package/frontend/js/app.js +192 -2
  12. package/frontend/js/components/connectionCard.js +3 -1
  13. package/frontend/js/components/modal.js +356 -15
  14. package/frontend/js/components/sidebar.js +41 -7
  15. package/frontend/js/components/topNav.js +2 -14
  16. package/frontend/js/router.js +10 -0
  17. package/frontend/js/store.js +448 -0
  18. package/frontend/js/utils/inputClear.js +36 -0
  19. package/frontend/js/utils/syntheticData.js +240 -0
  20. package/frontend/js/views/backups.js +52 -0
  21. package/frontend/js/views/data.js +16 -0
  22. package/frontend/js/views/logs.js +339 -0
  23. package/frontend/js/views/settings.js +77 -27
  24. package/frontend/js/views/structure.js +6 -0
  25. package/frontend/js/views/tableAdvisor.js +385 -0
  26. package/frontend/js/views/tableDesigner.js +6 -0
  27. package/frontend/styles/components.css +149 -0
  28. package/frontend/styles/tailwind.generated.css +1 -1
  29. package/frontend/styles/views.css +8 -0
  30. package/package.json +1 -1
  31. package/server/routes/data.js +45 -0
  32. package/server/routes/externalApi.js +285 -2
  33. package/server/routes/logs.js +127 -0
  34. package/server/server.js +3 -0
  35. package/server/services/databaseCommandService.js +45 -0
  36. package/server/services/sqlite/dataBrowserService.js +36 -0
  37. package/server/services/sqlite/introspection.js +226 -22
  38. package/server/services/sqlite/syntheticDataGenerator.js +728 -0
  39. package/server/services/sqlite/tableAdvisor.js +790 -0
  40. 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 parseSqlStringList(text = "") {
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
- if (text[index] !== "'") {
193
+ const character = text[index];
194
+
195
+ if (/\s|,/.test(character)) {
169
196
  index += 1;
170
197
  continue;
171
198
  }
172
199
 
173
- let value = "";
174
- index += 1;
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
- index += 1;
185
- values.push(value);
186
- break;
203
+ if (!parsed) {
204
+ return [];
187
205
  }
188
206
 
189
- value += text[index];
190
- index += 1;
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 = parseSqlStringList(expression.slice(openIndex + 1, closeIndex));
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
- ...column,
342
- allowedValues: allowedValuesByColumn.get(column.name) ?? [],
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()