sqlite-hub 0.4.0 → 0.6.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/README.md +2 -2
  2. package/changelog.md +15 -0
  3. package/frontend/assets/mockups/connections.png +0 -0
  4. package/frontend/assets/mockups/data.png +0 -0
  5. package/frontend/assets/mockups/data_row_editor.png +0 -0
  6. package/frontend/assets/mockups/home.png +0 -0
  7. package/frontend/assets/mockups/sql_editor.png +0 -0
  8. package/frontend/assets/mockups/sql_editor_querydetail.png +0 -0
  9. package/frontend/assets/mockups/structure.png +0 -0
  10. package/frontend/assets/mockups/structure_inspector.png +0 -0
  11. package/frontend/js/api.js +114 -5
  12. package/frontend/js/app.js +368 -18
  13. package/frontend/js/components/bottomTabs.js +1 -1
  14. package/frontend/js/components/dataGrid.js +3 -3
  15. package/frontend/js/components/queryEditor.js +33 -55
  16. package/frontend/js/components/queryHistoryDetail.js +263 -0
  17. package/frontend/js/components/queryHistoryPanel.js +228 -0
  18. package/frontend/js/components/queryResults.js +32 -46
  19. package/frontend/js/components/rowEditorPanel.js +73 -14
  20. package/frontend/js/components/sidebar.js +1 -0
  21. package/frontend/js/components/tableDesignerEditor.js +356 -0
  22. package/frontend/js/components/tableDesignerSidebar.js +126 -0
  23. package/frontend/js/components/tableDesignerSqlPreview.js +40 -0
  24. package/frontend/js/router.js +10 -0
  25. package/frontend/js/store.js +841 -22
  26. package/frontend/js/utils/format.js +23 -0
  27. package/frontend/js/utils/tableDesigner.js +1192 -0
  28. package/frontend/js/views/data.js +273 -250
  29. package/frontend/js/views/editor.js +34 -10
  30. package/frontend/js/views/overview.js +15 -0
  31. package/frontend/js/views/tableDesigner.js +37 -0
  32. package/frontend/styles/base.css +87 -73
  33. package/frontend/styles/components.css +841 -188
  34. package/frontend/styles/views.css +40 -0
  35. package/package.json +1 -1
  36. package/server/routes/data.js +2 -0
  37. package/server/routes/export.js +4 -1
  38. package/server/routes/overview.js +12 -0
  39. package/server/routes/sql.js +163 -5
  40. package/server/routes/tableDesigner.js +60 -0
  41. package/server/server.js +5 -1
  42. package/server/services/sqlite/dataBrowserService.js +4 -16
  43. package/server/services/sqlite/exportService.js +4 -16
  44. package/server/services/sqlite/overviewService.js +34 -0
  45. package/server/services/sqlite/sqlExecutor.js +83 -63
  46. package/server/services/sqlite/tableDesigner/changeAnalysis.js +295 -0
  47. package/server/services/sqlite/tableDesigner/schemaMapping.js +233 -0
  48. package/server/services/sqlite/tableDesigner/sql.js +63 -0
  49. package/server/services/sqlite/tableDesigner/validation.js +245 -0
  50. package/server/services/sqlite/tableDesignerService.js +181 -0
  51. package/server/services/sqlite/tableSort.js +63 -0
  52. package/server/services/storage/appStateStore.js +674 -1
  53. package/server/services/storage/queryHistoryUtils.js +169 -0
  54. package/frontend/assets/mockups/data_edit.png +0 -0
  55. package/frontend/assets/mockups/graph_visualize.png +0 -0
  56. package/frontend/assets/mockups/overview.png +0 -0
@@ -1,5 +1,4 @@
1
- const crypto = require("node:crypto");
2
- const { ValidationError } = require("../../utils/errors");
1
+ const { ValidationError, mapSqliteError } = require("../../utils/errors");
3
2
  const { serializeRows } = require("../../utils/sqliteTypes");
4
3
  const { getTableDetail } = require("./introspection");
5
4
 
@@ -151,7 +150,7 @@ function splitSqlStatements(sql) {
151
150
  statements.push(current.trim());
152
151
  }
153
152
 
154
- return statements.filter((statement) => statement.trim());
153
+ return statements.filter((statement) => statement.trim() && getFirstKeyword(statement));
155
154
  }
156
155
 
157
156
  function buildRowIdentity(tableDetail, row) {
@@ -337,62 +336,85 @@ class SqlExecutor {
337
336
  const results = [];
338
337
  let lastResultSet = null;
339
338
  let totalChanges = 0;
339
+ try {
340
+ statements.forEach((statement, index) => {
341
+ const prepared = db.prepare(statement);
342
+ const keyword = getFirstKeyword(statement);
343
+
344
+ if (options.requireReader && !prepared.reader) {
345
+ throw new ValidationError(
346
+ `Statement ${index + 1} is not a result-set statement and cannot be exported.`
347
+ );
348
+ }
340
349
 
341
- statements.forEach((statement, index) => {
342
- const prepared = db.prepare(statement);
343
- const keyword = getFirstKeyword(statement);
344
-
345
- if (options.requireReader && !prepared.reader) {
346
- throw new ValidationError(
347
- `Statement ${index + 1} is not a result-set statement and cannot be exported.`
348
- );
349
- }
350
+ if (prepared.reader) {
351
+ const rows = prepared.all();
352
+ const columnDefinitions = prepared.columns();
353
+ const serializedRows = serializeRows(rows);
354
+ const editableResult = resolveEditableResult(db, columnDefinitions, serializedRows);
355
+ const columns = columnDefinitions.map((column) => column.name);
356
+ const result = {
357
+ index,
358
+ sql: statement,
359
+ keyword,
360
+ kind: "resultSet",
361
+ rowCount: serializedRows.length,
362
+ columns,
363
+ rows: editableResult?.rows ?? serializedRows,
364
+ editing: editableResult
365
+ ? {
366
+ enabled: editableResult.enabled,
367
+ tableName: editableResult.tableName ?? null,
368
+ reason: editableResult.reason ?? "",
369
+ columns: editableResult.columns ?? [],
370
+ identityStrategy: editableResult.identityStrategy ?? null,
371
+ }
372
+ : null,
373
+ };
374
+ results.push(result);
375
+ lastResultSet = result;
376
+ return;
377
+ }
350
378
 
351
- if (prepared.reader) {
352
- const rows = prepared.all();
353
- const columnDefinitions = prepared.columns();
354
- const serializedRows = serializeRows(rows);
355
- const editableResult = resolveEditableResult(db, columnDefinitions, serializedRows);
356
- const columns = columnDefinitions.map((column) => column.name);
357
- const result = {
379
+ const info = prepared.run();
380
+ totalChanges += info.changes;
381
+ results.push({
358
382
  index,
359
383
  sql: statement,
360
384
  keyword,
361
- kind: "resultSet",
362
- rowCount: serializedRows.length,
363
- columns,
364
- rows: editableResult?.rows ?? serializedRows,
365
- editing: editableResult
366
- ? {
367
- enabled: editableResult.enabled,
368
- tableName: editableResult.tableName ?? null,
369
- reason: editableResult.reason ?? "",
370
- columns: editableResult.columns ?? [],
371
- identityStrategy: editableResult.identityStrategy ?? null,
372
- }
373
- : null,
374
- };
375
- results.push(result);
376
- lastResultSet = result;
377
- return;
385
+ kind: "mutation",
386
+ changes: info.changes,
387
+ lastInsertRowid:
388
+ keyword === "INSERT" || keyword === "REPLACE"
389
+ ? typeof info.lastInsertRowid === "bigint"
390
+ ? Number(info.lastInsertRowid)
391
+ : info.lastInsertRowid
392
+ : null,
393
+ });
394
+ });
395
+ } catch (error) {
396
+ const normalizedError = mapSqliteError(error);
397
+
398
+ if (options.persistHistory !== false) {
399
+ try {
400
+ this.appStateStore.recordQueryExecution({
401
+ databaseKey: connection.id,
402
+ rawSql: sql,
403
+ status: "error",
404
+ durationMs: Date.now() - startedAt,
405
+ rowCount: lastResultSet?.rowCount ?? null,
406
+ affectedRows: totalChanges,
407
+ errorMessage: normalizedError.message,
408
+ });
409
+ } catch (recordingError) {
410
+ console.warn(
411
+ `Failed to record SQL query history error for ${connection.id}: ${recordingError.message}`
412
+ );
413
+ }
378
414
  }
379
415
 
380
- const info = prepared.run();
381
- totalChanges += info.changes;
382
- results.push({
383
- index,
384
- sql: statement,
385
- keyword,
386
- kind: "mutation",
387
- changes: info.changes,
388
- lastInsertRowid:
389
- keyword === "INSERT" || keyword === "REPLACE"
390
- ? typeof info.lastInsertRowid === "bigint"
391
- ? Number(info.lastInsertRowid)
392
- : info.lastInsertRowid
393
- : null,
394
- });
395
- });
416
+ throw normalizedError;
417
+ }
396
418
 
397
419
  const timingMs = Date.now() - startedAt;
398
420
  const payload = {
@@ -405,25 +427,23 @@ class SqlExecutor {
405
427
  affectedRowCount: totalChanges,
406
428
  resultKind: lastResultSet ? "resultSet" : results.at(-1)?.kind ?? "unknown",
407
429
  };
430
+ let historyId = null;
408
431
 
409
432
  if (options.persistHistory !== false) {
410
- this.appStateStore.addSqlHistory({
411
- id: crypto.randomUUID(),
412
- connectionId: connection.id,
413
- connectionLabel: connection.label,
414
- sql,
415
- statementCount: statements.length,
416
- resultKind: payload.resultKind,
417
- affectedRowCount: totalChanges,
418
- rowCount: lastResultSet?.rowCount ?? 0,
419
- timingMs,
420
- executedAt: new Date().toISOString(),
433
+ historyId = this.appStateStore.recordQueryExecution({
434
+ databaseKey: connection.id,
435
+ rawSql: sql,
436
+ status: "success",
437
+ durationMs: timingMs,
438
+ rowCount: lastResultSet?.rowCount ?? null,
439
+ affectedRows: totalChanges,
421
440
  });
422
441
  }
423
442
 
424
443
  return {
425
444
  ...payload,
426
445
  timingMs,
446
+ historyId,
427
447
  };
428
448
  }
429
449
  }
@@ -0,0 +1,295 @@
1
+ const {
2
+ buildAlterTableAddColumnSql,
3
+ buildAlterTableRenameSql,
4
+ buildCreateTableSql,
5
+ } = require("./sql");
6
+ const { createDesignerWarning } = require("./schemaMapping");
7
+ const { normalizeIdentifierKey, normalizeSqlFragment } = require("./validation");
8
+
9
+ function normalizeComparableValue(value) {
10
+ return normalizeSqlFragment(value);
11
+ }
12
+
13
+ function hasColumnChanged(originalColumn, draftColumn) {
14
+ return (
15
+ normalizeIdentifierKey(originalColumn.name) !== normalizeIdentifierKey(draftColumn.name) ||
16
+ normalizeComparableValue(originalColumn.type) !== normalizeComparableValue(draftColumn.type) ||
17
+ Boolean(originalColumn.notNull) !== Boolean(draftColumn.notNull) ||
18
+ Boolean(originalColumn.unique) !== Boolean(draftColumn.unique) ||
19
+ Boolean(originalColumn.primaryKey) !== Boolean(draftColumn.primaryKey) ||
20
+ normalizeComparableValue(originalColumn.defaultValue) !==
21
+ normalizeComparableValue(draftColumn.defaultValue) ||
22
+ normalizeIdentifierKey(originalColumn.referencesTable) !==
23
+ normalizeIdentifierKey(draftColumn.referencesTable) ||
24
+ normalizeIdentifierKey(originalColumn.referencesColumn) !==
25
+ normalizeIdentifierKey(draftColumn.referencesColumn)
26
+ );
27
+ }
28
+
29
+ function hasMeaningfulNewDraftContent(draft) {
30
+ return (
31
+ Boolean(String(draft.tableName ?? "").trim()) ||
32
+ draft.columns.some((column) =>
33
+ !column.deleted &&
34
+ [
35
+ Boolean(String(column.name ?? "").trim()),
36
+ Boolean(String(column.defaultValue ?? "").trim()),
37
+ Boolean(String(column.referencesTable ?? "").trim()),
38
+ Boolean(String(column.referencesColumn ?? "").trim()),
39
+ Boolean(column.notNull),
40
+ Boolean(column.unique),
41
+ Boolean(column.primaryKey),
42
+ String(column.type ?? "").trim().toUpperCase() !== "TEXT",
43
+ ].some(Boolean)
44
+ )
45
+ );
46
+ }
47
+
48
+ function isAlterAddColumnSafe(column) {
49
+ const defaultValue = normalizeComparableValue(column.defaultValue);
50
+ const normalizedDefault = defaultValue.toUpperCase();
51
+
52
+ if (column.primaryKey) {
53
+ return {
54
+ safe: false,
55
+ reason: `Column ${column.name} cannot be added with PRIMARY KEY via ALTER TABLE in SQLite.`,
56
+ };
57
+ }
58
+
59
+ if (column.unique) {
60
+ return {
61
+ safe: false,
62
+ reason: `Column ${column.name} cannot be added with UNIQUE via ALTER TABLE in SQLite.`,
63
+ };
64
+ }
65
+
66
+ if (column.notNull && !defaultValue) {
67
+ return {
68
+ safe: false,
69
+ reason:
70
+ `Column ${column.name} uses NOT NULL without a default value. SQLite requires a table rebuild for that change.`,
71
+ };
72
+ }
73
+
74
+ if (["CURRENT_TIME", "CURRENT_DATE", "CURRENT_TIMESTAMP"].includes(normalizedDefault)) {
75
+ return {
76
+ safe: false,
77
+ reason:
78
+ `Column ${column.name} uses a dynamic default value that SQLite does not allow in ALTER TABLE ADD COLUMN.`,
79
+ };
80
+ }
81
+
82
+ if (/^\(.+\)$/.test(defaultValue)) {
83
+ return {
84
+ safe: false,
85
+ reason:
86
+ `Column ${column.name} uses an expression default value. SQLite requires a rebuild for that ADD COLUMN operation.`,
87
+ };
88
+ }
89
+
90
+ if (
91
+ column.referencesTable &&
92
+ column.referencesColumn &&
93
+ defaultValue &&
94
+ normalizedDefault !== "NULL"
95
+ ) {
96
+ return {
97
+ safe: false,
98
+ reason:
99
+ `Column ${column.name} adds a foreign key with a non-NULL default value. SQLite requires a table rebuild for that change.`,
100
+ };
101
+ }
102
+
103
+ return {
104
+ safe: true,
105
+ reason: "",
106
+ };
107
+ }
108
+
109
+ function buildRiskyChangeWarning(title, message) {
110
+ return createDesignerWarning({
111
+ code: "TABLE_REBUILD_REQUIRED",
112
+ title,
113
+ message,
114
+ blocking: true,
115
+ });
116
+ }
117
+
118
+ function analyzeEditDraft(draft, originalDraft) {
119
+ const originalColumnsByName = new Map(
120
+ originalDraft.columns.map((column) => [normalizeIdentifierKey(column.originalName || column.name), column])
121
+ );
122
+ const matchedOriginalNames = new Set();
123
+ const newColumns = [];
124
+ const warnings = [];
125
+ const executableStatements = [];
126
+
127
+ draft.columns.forEach((column) => {
128
+ if (column.deleted) {
129
+ if (normalizeIdentifierKey(column.originalName || column.name)) {
130
+ warnings.push(
131
+ buildRiskyChangeWarning(
132
+ "Column Delete Requires Rebuild",
133
+ `Deleting column ${column.originalName || column.name} requires a SQLite table rebuild.`
134
+ )
135
+ );
136
+ }
137
+
138
+ return;
139
+ }
140
+
141
+ const originalNameKey = normalizeIdentifierKey(column.originalName);
142
+
143
+ if (originalNameKey && originalColumnsByName.has(originalNameKey)) {
144
+ const originalColumn = originalColumnsByName.get(originalNameKey);
145
+ matchedOriginalNames.add(originalNameKey);
146
+
147
+ if (hasColumnChanged(originalColumn, column)) {
148
+ if (normalizeIdentifierKey(originalColumn.name) !== normalizeIdentifierKey(column.name)) {
149
+ warnings.push(
150
+ buildRiskyChangeWarning(
151
+ "Column Rename Requires Rebuild",
152
+ `Renaming column ${originalColumn.name} to ${column.name} is intentionally blocked in Table Designer v1.`
153
+ )
154
+ );
155
+ }
156
+
157
+ if (normalizeComparableValue(originalColumn.type) !== normalizeComparableValue(column.type)) {
158
+ warnings.push(
159
+ buildRiskyChangeWarning(
160
+ "Column Type Change Requires Rebuild",
161
+ `Changing the type of ${originalColumn.name} from ${originalColumn.type} to ${column.type} requires a SQLite table rebuild.`
162
+ )
163
+ );
164
+ }
165
+
166
+ if (Boolean(originalColumn.notNull) !== Boolean(column.notNull)) {
167
+ warnings.push(
168
+ buildRiskyChangeWarning(
169
+ "NOT NULL Change Requires Rebuild",
170
+ `Changing the NOT NULL setting on ${originalColumn.name} requires a SQLite table rebuild.`
171
+ )
172
+ );
173
+ }
174
+
175
+ if (Boolean(originalColumn.unique) !== Boolean(column.unique)) {
176
+ warnings.push(
177
+ buildRiskyChangeWarning(
178
+ "UNIQUE Change Requires Rebuild",
179
+ `Changing the UNIQUE setting on ${originalColumn.name} requires a SQLite table rebuild.`
180
+ )
181
+ );
182
+ }
183
+
184
+ if (Boolean(originalColumn.primaryKey) !== Boolean(column.primaryKey)) {
185
+ warnings.push(
186
+ buildRiskyChangeWarning(
187
+ "Primary Key Change Requires Rebuild",
188
+ `Changing the primary key definition on ${originalColumn.name} requires a SQLite table rebuild.`
189
+ )
190
+ );
191
+ }
192
+
193
+ if (
194
+ normalizeComparableValue(originalColumn.defaultValue) !==
195
+ normalizeComparableValue(column.defaultValue)
196
+ ) {
197
+ warnings.push(
198
+ buildRiskyChangeWarning(
199
+ "Default Value Change Requires Rebuild",
200
+ `Changing the default value on ${originalColumn.name} requires a SQLite table rebuild.`
201
+ )
202
+ );
203
+ }
204
+
205
+ if (
206
+ normalizeIdentifierKey(originalColumn.referencesTable) !==
207
+ normalizeIdentifierKey(column.referencesTable) ||
208
+ normalizeIdentifierKey(originalColumn.referencesColumn) !==
209
+ normalizeIdentifierKey(column.referencesColumn)
210
+ ) {
211
+ warnings.push(
212
+ buildRiskyChangeWarning(
213
+ "Foreign Key Change Requires Rebuild",
214
+ `Changing the foreign key on ${originalColumn.name} requires a SQLite table rebuild.`
215
+ )
216
+ );
217
+ }
218
+ }
219
+
220
+ return;
221
+ }
222
+
223
+ newColumns.push(column);
224
+ });
225
+
226
+ originalDraft.columns.forEach((column) => {
227
+ const key = normalizeIdentifierKey(column.originalName || column.name);
228
+ const isExplicitlyDeleted = draft.columns.some(
229
+ (draftColumn) =>
230
+ draftColumn.deleted &&
231
+ normalizeIdentifierKey(draftColumn.originalName || draftColumn.name) === key
232
+ );
233
+
234
+ if (!matchedOriginalNames.has(key) && !isExplicitlyDeleted) {
235
+ warnings.push(
236
+ buildRiskyChangeWarning(
237
+ "Column Delete Requires Rebuild",
238
+ `Deleting column ${column.name} requires a SQLite table rebuild.`
239
+ )
240
+ );
241
+ }
242
+ });
243
+
244
+ const tableRenameChanged =
245
+ normalizeIdentifierKey(draft.tableName) !== normalizeIdentifierKey(originalDraft.tableName);
246
+
247
+ if (tableRenameChanged) {
248
+ executableStatements.push(
249
+ buildAlterTableRenameSql(originalDraft.tableName, draft.tableName)
250
+ );
251
+ }
252
+
253
+ newColumns.forEach((column) => {
254
+ const addColumnSafety = isAlterAddColumnSafe(column);
255
+
256
+ if (!addColumnSafety.safe) {
257
+ warnings.push(
258
+ buildRiskyChangeWarning("Column Add Requires Rebuild", addColumnSafety.reason)
259
+ );
260
+ return;
261
+ }
262
+
263
+ executableStatements.push(
264
+ buildAlterTableAddColumnSql(tableRenameChanged ? draft.tableName : originalDraft.tableName, column)
265
+ );
266
+ });
267
+
268
+ const dirty = Boolean(executableStatements.length || warnings.length);
269
+
270
+ return {
271
+ dirty,
272
+ executable: warnings.length === 0,
273
+ statements: warnings.length === 0 ? executableStatements : [],
274
+ previewStatements: executableStatements,
275
+ warnings,
276
+ };
277
+ }
278
+
279
+ function analyzeTableDesignerChanges({ draft, originalDraft = null }) {
280
+ if (!originalDraft) {
281
+ return {
282
+ dirty: hasMeaningfulNewDraftContent(draft),
283
+ executable: true,
284
+ statements: [buildCreateTableSql(draft)],
285
+ previewStatements: [buildCreateTableSql(draft)],
286
+ warnings: [],
287
+ };
288
+ }
289
+
290
+ return analyzeEditDraft(draft, originalDraft);
291
+ }
292
+
293
+ module.exports = {
294
+ analyzeTableDesignerChanges,
295
+ };