sqlite-hub 2.0.1 → 2.1.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 (85) hide show
  1. package/README.md +14 -9
  2. package/docs/MCP.md +8 -0
  3. package/docs/changelog.md +10 -0
  4. package/docs/guidelines/DESIGN.md +64 -0
  5. package/frontend/assets/mockups/backups_1_1920.webp +0 -0
  6. package/frontend/assets/mockups/backups_2_compare_drawer_1920.webp +0 -0
  7. package/frontend/assets/mockups/backups_3_create_backup_modal_1920.webp +0 -0
  8. package/frontend/assets/mockups/backups_4_edit_backup_modal_1920.webp +0 -0
  9. package/frontend/assets/mockups/backups_5_restore_backup_modal_1920.webp +0 -0
  10. package/frontend/assets/mockups/backups_6_delete_backup_modal_1920.webp +0 -0
  11. package/frontend/assets/mockups/charts_1_1920.webp +0 -0
  12. package/frontend/assets/mockups/charts_2_query_detail_1920.webp +0 -0
  13. package/frontend/assets/mockups/charts_3_create_query_chart_modal_1920.webp +0 -0
  14. package/frontend/assets/mockups/charts_4_edit_query_chart_modal_1920.webp +0 -0
  15. package/frontend/assets/mockups/charts_5_delete_query_chart_modal_1920.webp +0 -0
  16. package/frontend/assets/mockups/charts_6_copy_column_modal_1920.webp +0 -0
  17. package/frontend/assets/mockups/connections_1_1920.webp +0 -0
  18. package/frontend/assets/mockups/connections_2_create_connection_modal_1920.webp +0 -0
  19. package/frontend/assets/mockups/connections_3_open_connection_modal_1920.webp +0 -0
  20. package/frontend/assets/mockups/data_1_1920.webp +0 -0
  21. package/frontend/assets/mockups/data_2_roweditor_1920.webp +0 -0
  22. package/frontend/assets/mockups/data_3_generate_data_modal_1920.webp +0 -0
  23. package/frontend/assets/mockups/data_4_data_export_modal_1920.webp +0 -0
  24. package/frontend/assets/mockups/documents_1_1920.webp +0 -0
  25. package/frontend/assets/mockups/documents_2_document_insert_table_modal_1920.webp +0 -0
  26. package/frontend/assets/mockups/documents_3_document_insert_note_modal_1920.webp +0 -0
  27. package/frontend/assets/mockups/logs_1_1920.webp +0 -0
  28. package/frontend/assets/mockups/media_tagging_queue_1_1920.webp +0 -0
  29. package/frontend/assets/mockups/media_tagging_setup_1_1920.webp +0 -0
  30. package/frontend/assets/mockups/media_tagging_setup_2_create_media_tagging_tag_table_modal_1920.webp +0 -0
  31. package/frontend/assets/mockups/media_tagging_setup_3_create_media_tagging_mapping_table_modal_1920.webp +0 -0
  32. package/frontend/assets/mockups/overview_1_1920.webp +0 -0
  33. package/frontend/assets/mockups/settings_1_1920.webp +0 -0
  34. package/frontend/assets/mockups/sql_editor_1_1920.webp +0 -0
  35. package/frontend/assets/mockups/sql_editor_2_query_detail_1920.webp +0 -0
  36. package/frontend/assets/mockups/sql_editor_3_query_export_modal_1920.webp +0 -0
  37. package/frontend/assets/mockups/structure_1_1920.webp +0 -0
  38. package/frontend/assets/mockups/structure_2_generate_types_modal_1920.webp +0 -0
  39. package/frontend/assets/mockups/structure_3_generate_types_modal_1920.webp +0 -0
  40. package/frontend/assets/mockups/table_designer_1_1920.webp +0 -0
  41. package/frontend/assets/mockups/table_designer_2_table_designer_constraints_modal_1920.webp +0 -0
  42. package/frontend/js/api.js +11 -0
  43. package/frontend/js/app.js +238 -8
  44. package/frontend/js/components/badges.js +16 -0
  45. package/frontend/js/components/connectionCard.js +35 -0
  46. package/frontend/js/components/modal.js +116 -245
  47. package/frontend/js/components/queryHistoryDetail.js +25 -45
  48. package/frontend/js/components/rowEditorPanel.js +7 -60
  49. package/frontend/js/components/structureGraph.js +292 -13
  50. package/frontend/js/components/tableDesignerConstraintsDrawer.js +400 -0
  51. package/frontend/js/components/tableDesignerEditor.js +51 -16
  52. package/frontend/js/components/tableDesignerSidebar.js +9 -5
  53. package/frontend/js/store.js +551 -7
  54. package/frontend/js/utils/connectionRegistry.js +76 -0
  55. package/frontend/js/utils/tableDesigner.js +431 -62
  56. package/frontend/js/views/backups.js +2 -5
  57. package/frontend/js/views/charts.js +20 -10
  58. package/frontend/js/views/connections.js +103 -3
  59. package/frontend/js/views/data.js +19 -5
  60. package/frontend/js/views/structure.js +28 -5
  61. package/frontend/js/views/tableAdvisor.js +14 -6
  62. package/frontend/js/views/tableDesigner.js +44 -9
  63. package/frontend/styles/components.css +676 -79
  64. package/frontend/styles/structure-graph.css +49 -0
  65. package/frontend/styles/tailwind.generated.css +1 -1
  66. package/package.json +1 -1
  67. package/server/routes/connections.js +12 -0
  68. package/server/routes/data.js +1 -1
  69. package/server/routes/tableDesigner.js +15 -1
  70. package/server/services/mcpToolService.js +28 -0
  71. package/server/services/sqlite/connectionManager.js +23 -2
  72. package/server/services/sqlite/dataBrowserService.js +21 -0
  73. package/server/services/sqlite/introspection.js +39 -1
  74. package/server/services/sqlite/structureService.js +66 -11
  75. package/server/services/sqlite/tableAdvisor.js +6 -0
  76. package/server/services/sqlite/tableDesigner/schemaMapping.js +7 -1
  77. package/server/services/sqlite/tableDesigner/sql.js +31 -2
  78. package/server/services/sqlite/tableDesigner/validation.js +69 -9
  79. package/server/services/sqlite/tableDesignerService.js +110 -5
  80. package/server/services/storage/appStateStore.js +303 -4
  81. package/frontend/assets/mockups/backups_2_create_backup_modal_1920.webp +0 -0
  82. package/frontend/assets/mockups/backups_3_edit_backup_modal_1920.webp +0 -0
  83. package/frontend/assets/mockups/backups_4_restore_backup_modal_1920.webp +0 -0
  84. package/frontend/assets/mockups/backups_5_delete_backup_modal_1920.webp +0 -0
  85. package/frontend/assets/mockups/data_3_data_export_modal_1920.webp +0 -0
@@ -19,6 +19,37 @@ function getTableListMap(db) {
19
19
  }
20
20
  }
21
21
 
22
+ function normalizeTableKind(entry = {}, tableListEntry = null) {
23
+ const pragmaType = String(tableListEntry?.type ?? "")
24
+ .trim()
25
+ .toLowerCase();
26
+
27
+ if (pragmaType) {
28
+ return pragmaType;
29
+ }
30
+
31
+ if (entry.type === "table" && /^\s*CREATE\s+VIRTUAL\s+TABLE\b/i.test(entry.sql || "")) {
32
+ return "virtual";
33
+ }
34
+
35
+ return entry.type;
36
+ }
37
+
38
+ function decorateStructureEntry(entry, tableListMap = null) {
39
+ if (entry.type !== "table") {
40
+ return entry;
41
+ }
42
+
43
+ const tableKind = normalizeTableKind(entry, tableListMap?.get(entry.name));
44
+
45
+ return {
46
+ ...entry,
47
+ tableKind,
48
+ isVirtual: tableKind === "virtual",
49
+ isShadow: tableKind === "shadow",
50
+ };
51
+ }
52
+
22
53
  function getMasterEntry(db, type, name) {
23
54
  const entry = db
24
55
  .prepare(
@@ -34,6 +65,8 @@ function getMasterEntry(db, type, name) {
34
65
  }
35
66
 
36
67
  function getRawStructureEntries(db) {
68
+ const tableListMap = getTableListMap(db);
69
+
37
70
  return db
38
71
  .prepare(
39
72
  [
@@ -43,7 +76,8 @@ function getRawStructureEntries(db) {
43
76
  "ORDER BY type ASC, name ASC",
44
77
  ].join(" ")
45
78
  )
46
- .all();
79
+ .all()
80
+ .map((entry) => decorateStructureEntry(entry, tableListMap));
47
81
  }
48
82
 
49
83
  function normalizeColumn(column, visibleSet) {
@@ -607,11 +641,15 @@ function getTableDetail(db, tableName, options = {}) {
607
641
  typeof tableListEntry?.wr === "number"
608
642
  ? Boolean(tableListEntry.wr)
609
643
  : /WITHOUT\s+ROWID/i.test(entry.sql || "");
644
+ const tableKind = normalizeTableKind(entry, tableListEntry);
610
645
 
611
646
  const tableDetail = {
612
647
  type: entry.type,
613
648
  name: entry.name,
614
649
  ddl: entry.sql,
650
+ tableKind,
651
+ isVirtual: tableKind === "virtual",
652
+ isShadow: tableKind === "shadow",
615
653
  withoutRowId,
616
654
  strict: Boolean(tableListEntry?.strict),
617
655
  columns,
@@ -3,6 +3,65 @@ const { quoteIdentifier } = require("../../utils/identifier");
3
3
  const { serializeRows } = require("../../utils/sqliteTypes");
4
4
  const { TypeGenerationService } = require("../typeGenerationService");
5
5
 
6
+ function resolveShadowOwnerTableName(shadowTable, tables) {
7
+ if (!shadowTable?.isShadow) {
8
+ return null;
9
+ }
10
+
11
+ return (
12
+ tables
13
+ .filter((table) => table.isVirtual && shadowTable.name.startsWith(`${table.name}_`))
14
+ .sort((left, right) => right.name.length - left.name.length)[0]?.name ?? null
15
+ );
16
+ }
17
+
18
+ function unquoteSqlIdentifier(identifier) {
19
+ const value = String(identifier ?? "").trim();
20
+
21
+ if (value.length >= 2 && value[0] === '"' && value.at(-1) === '"') {
22
+ return value.slice(1, -1).replace(/""/g, '"');
23
+ }
24
+
25
+ if (value.length >= 2 && value[0] === "`" && value.at(-1) === "`") {
26
+ return value.slice(1, -1).replace(/``/g, "`");
27
+ }
28
+
29
+ if (value.length >= 2 && value[0] === "[" && value.at(-1) === "]") {
30
+ return value.slice(1, -1);
31
+ }
32
+
33
+ return value;
34
+ }
35
+
36
+ function extractVirtualTableModule(ddl = "") {
37
+ const match = String(ddl ?? "").match(
38
+ /\bUSING\s+("[^"]+(?:""[^"]*)*"|`[^`]+(?:``[^`]*)*`|\[[^\]]+\]|[A-Za-z_][A-Za-z0-9_]*)/i
39
+ );
40
+
41
+ return match ? unquoteSqlIdentifier(match[1]) : null;
42
+ }
43
+
44
+ function serializeGraphTable(table, shadowOwnerTableName = null) {
45
+ const isVirtual = Boolean(table.isVirtual);
46
+
47
+ return {
48
+ type: table.type,
49
+ name: table.name,
50
+ ddl: table.ddl,
51
+ tableKind: table.tableKind,
52
+ isVirtual,
53
+ isShadow: Boolean(table.isShadow),
54
+ virtualModule: isVirtual ? extractVirtualTableModule(table.ddl) : null,
55
+ shadowOwnerTable: shadowOwnerTableName,
56
+ withoutRowId: table.withoutRowId,
57
+ strict: table.strict,
58
+ columns: table.columns,
59
+ foreignKeys: table.foreignKeys,
60
+ identityStrategy: table.identityStrategy,
61
+ notSafelyUpdatable: table.notSafelyUpdatable,
62
+ };
63
+ }
64
+
6
65
  class StructureService {
7
66
  constructor({ connectionManager, appStateStore }) {
8
67
  this.connectionManager = connectionManager;
@@ -35,17 +94,9 @@ class StructureService {
35
94
  triggers: entries.filter((entry) => entry.type === "trigger"),
36
95
  },
37
96
  graph: {
38
- tables: tables.map((table) => ({
39
- type: table.type,
40
- name: table.name,
41
- ddl: table.ddl,
42
- withoutRowId: table.withoutRowId,
43
- strict: table.strict,
44
- columns: table.columns,
45
- foreignKeys: table.foreignKeys,
46
- identityStrategy: table.identityStrategy,
47
- notSafelyUpdatable: table.notSafelyUpdatable,
48
- })),
97
+ tables: tables.map((table) =>
98
+ serializeGraphTable(table, resolveShadowOwnerTableName(table, tables))
99
+ ),
49
100
  relationshipCount,
50
101
  },
51
102
  };
@@ -65,6 +116,10 @@ class StructureService {
65
116
  type: table.type,
66
117
  name: table.name,
67
118
  ddl: table.ddl,
119
+ tableKind: table.tableKind,
120
+ isVirtual: Boolean(table.isVirtual),
121
+ isShadow: Boolean(table.isShadow),
122
+ virtualModule: table.isVirtual ? extractVirtualTableModule(table.ddl) : null,
68
123
  withoutRowId: table.withoutRowId,
69
124
  strict: table.strict,
70
125
  columns: table.columns,
@@ -763,12 +763,18 @@ function analyzeTable(db, tableName) {
763
763
  score: calculateScore(issues),
764
764
  issueCount: issues.length,
765
765
  rowCount: Number(tableDetail.rowCount ?? 0),
766
+ tableKind: tableDetail.tableKind,
767
+ isVirtual: Boolean(tableDetail.isVirtual),
768
+ isShadow: Boolean(tableDetail.isShadow),
766
769
  issues,
767
770
  columnProfiles,
768
771
  table: {
769
772
  columnCount: (tableDetail.columns ?? []).filter((column) => column.visible && !column.generated).length,
770
773
  indexCount: (tableDetail.indexes ?? []).length,
771
774
  foreignKeyCount: (tableDetail.foreignKeys ?? []).length,
775
+ tableKind: tableDetail.tableKind,
776
+ isVirtual: Boolean(tableDetail.isVirtual),
777
+ isShadow: Boolean(tableDetail.isShadow),
772
778
  },
773
779
  analyzedAt: new Date().toISOString(),
774
780
  };
@@ -159,6 +159,9 @@ function buildCheckConstraints(tableDetail) {
159
159
  originalExpression: expression ? `CHECK (${expression})` : "CHECK constraint",
160
160
  editable: true,
161
161
  preserved: true,
162
+ source: "detected",
163
+ presetId: "",
164
+ presetFields: {},
162
165
  };
163
166
  });
164
167
  }
@@ -300,7 +303,7 @@ function buildTableDesignerDraft(tableDetail) {
300
303
 
301
304
  function listDesignerTables(db) {
302
305
  return getRawStructureEntries(db)
303
- .filter((entry) => entry.type === "table")
306
+ .filter((entry) => entry.type === "table" && !entry.isShadow)
304
307
  .map((entry) => {
305
308
  const columns = db
306
309
  .prepare(`PRAGMA table_xinfo(${quoteIdentifier(entry.name)})`)
@@ -312,6 +315,9 @@ function listDesignerTables(db) {
312
315
  name: entry.name,
313
316
  columnCount: columns.length,
314
317
  columns,
318
+ tableKind: entry.tableKind ?? "table",
319
+ isVirtual: Boolean(entry.isVirtual),
320
+ isShadow: Boolean(entry.isShadow),
315
321
  };
316
322
  })
317
323
  .sort((left, right) => left.name.localeCompare(right.name, undefined, { sensitivity: "base" }));
@@ -35,10 +35,39 @@ function buildColumnDefinition(column) {
35
35
  return parts.join(" ");
36
36
  }
37
37
 
38
+ function normalizeCheckExpressionSql(expression) {
39
+ const normalized = normalizeSqlFragment(expression);
40
+
41
+ if (!normalized) {
42
+ return "";
43
+ }
44
+
45
+ if (/^CHECK\s*\(/i.test(normalized)) {
46
+ return normalized;
47
+ }
48
+
49
+ return `CHECK (${normalized})`;
50
+ }
51
+
52
+ function buildCheckConstraintSql(constraint) {
53
+ if (constraint.deleted) {
54
+ return "";
55
+ }
56
+
57
+ return normalizeCheckExpressionSql(constraint.expression);
58
+ }
59
+
38
60
  function buildCreateTableSql(draft) {
39
- const columnSql = draft.columns.map((column) => ` ${buildColumnDefinition(column)}`).join(",\n");
61
+ const columnSql = draft.columns
62
+ .filter((column) => !column.deleted)
63
+ .map((column) => ` ${buildColumnDefinition(column)}`);
64
+ const checkSql = (draft.checkConstraints ?? [])
65
+ .map(buildCheckConstraintSql)
66
+ .filter(Boolean)
67
+ .map((constraintSql) => ` ${constraintSql}`);
68
+ const definitionSql = [...columnSql, ...checkSql].join(",\n");
40
69
 
41
- return `CREATE TABLE ${quoteIdentifier(draft.tableName)} (\n${columnSql}\n);`;
70
+ return `CREATE TABLE ${quoteIdentifier(draft.tableName)} (\n${definitionSql}\n);`;
42
71
  }
43
72
 
44
73
  function buildAlterTableRenameSql(fromName, toName) {
@@ -10,6 +10,36 @@ function normalizeSqlFragment(value) {
10
10
  return String(value ?? "").trim();
11
11
  }
12
12
 
13
+ function normalizeImportFormat(value) {
14
+ const normalized = String(value ?? "").trim().toLowerCase();
15
+
16
+ return ["csv", "tsv", "json"].includes(normalized) ? normalized : "";
17
+ }
18
+
19
+ function normalizeImportedCellValue(value) {
20
+ if (value === null || value === undefined) {
21
+ return null;
22
+ }
23
+
24
+ if (["boolean", "number", "string"].includes(typeof value)) {
25
+ return value;
26
+ }
27
+
28
+ try {
29
+ return JSON.stringify(value);
30
+ } catch {
31
+ return String(value);
32
+ }
33
+ }
34
+
35
+ function normalizeImportedRows(rows) {
36
+ return Array.isArray(rows)
37
+ ? rows.map((row) =>
38
+ Array.isArray(row) ? row.map((cell) => normalizeImportedCellValue(cell)) : []
39
+ )
40
+ : [];
41
+ }
42
+
13
43
  function assertSafeSqlFragment(value, label, { allowEmpty = true } = {}) {
14
44
  const normalized = normalizeSqlFragment(value);
15
45
 
@@ -96,6 +126,7 @@ function normalizeCheckConstraintPayload(constraint = {}, index = 0) {
96
126
  id: String(constraint.id ?? `check:${index}`),
97
127
  name: String(constraint.name ?? `CHECK ${index + 1}`).trim(),
98
128
  originalName: String(constraint.originalName ?? constraint.name ?? `CHECK ${index + 1}`).trim(),
129
+ deleted: normalizeBoolean(constraint.deleted),
99
130
  columns: Array.isArray(constraint.columns)
100
131
  ? constraint.columns
101
132
  .map((column) => ({
@@ -110,11 +141,28 @@ function normalizeCheckConstraintPayload(constraint = {}, index = 0) {
110
141
  originalExpression: String(constraint.originalExpression ?? constraint.expression ?? "").trim(),
111
142
  editable: normalizeBoolean(constraint.editable),
112
143
  preserved: constraint.preserved === undefined ? true : normalizeBoolean(constraint.preserved),
144
+ columnId: String(constraint.columnId ?? "").trim(),
145
+ source: String(constraint.source ?? (constraint.originalExpression ? "detected" : "user")).trim(),
146
+ presetId: String(constraint.presetId ?? "").trim(),
147
+ presetFields:
148
+ constraint.presetFields && typeof constraint.presetFields === "object"
149
+ ? Object.fromEntries(
150
+ Object.entries(constraint.presetFields).map(([key, value]) => [key, String(value ?? "")])
151
+ )
152
+ : {},
113
153
  };
114
154
  }
115
155
 
116
156
  function normalizeDraftPayload(payload = {}) {
117
157
  const mode = String(payload.mode ?? "create").trim() === "edit" ? "edit" : "create";
158
+ const importRows = normalizeImportedRows(payload.importRows ?? payload.importedCsvRows);
159
+ const importSourceFileName = String(
160
+ payload.importSourceFileName ?? payload.importedCsvFileName ?? ""
161
+ ).trim();
162
+ const importDelimiter = String(payload.importDelimiter ?? payload.importedCsvDelimiter ?? "").trim();
163
+ const importFormat = normalizeImportFormat(
164
+ payload.importFormat ?? (importSourceFileName ? "csv" : "")
165
+ );
118
166
 
119
167
  return {
120
168
  mode,
@@ -137,13 +185,13 @@ function normalizeDraftPayload(payload = {}) {
137
185
  schemaWarnings: Array.isArray(payload.schemaWarnings) ? payload.schemaWarnings : [],
138
186
  warnings: Array.isArray(payload.warnings) ? payload.warnings : [],
139
187
  fillImportedRows: normalizeBoolean(payload.fillImportedRows),
140
- importedCsvFileName: String(payload.importedCsvFileName ?? "").trim(),
141
- importedCsvDelimiter: String(payload.importedCsvDelimiter ?? "").trim(),
142
- importedCsvRows: Array.isArray(payload.importedCsvRows)
143
- ? payload.importedCsvRows.map((row) =>
144
- Array.isArray(row) ? row.map((cell) => String(cell ?? "")) : []
145
- )
146
- : [],
188
+ importFormat,
189
+ importSourceFileName,
190
+ importDelimiter,
191
+ importRows,
192
+ importedCsvFileName: importSourceFileName,
193
+ importedCsvDelimiter: importDelimiter,
194
+ importedCsvRows: importRows,
147
195
  dirty: normalizeBoolean(payload.dirty),
148
196
  };
149
197
  }
@@ -290,9 +338,21 @@ function validateTableDesignerDraft(draft, { catalogTables = [], originalDraft =
290
338
  throw new ValidationError("Imported row fill is only available when creating a table.");
291
339
  }
292
340
 
293
- if (draft.fillImportedRows && !draft.importedCsvRows.length) {
294
- throw new ValidationError("Fill requires imported CSV rows.");
341
+ if (draft.fillImportedRows && !draft.importRows.length) {
342
+ throw new ValidationError("Fill requires imported rows.");
295
343
  }
344
+
345
+ (draft.checkConstraints ?? [])
346
+ .filter((constraint) => !constraint.deleted)
347
+ .forEach((constraint) => {
348
+ const label = constraint.name || "CHECK constraint";
349
+
350
+ if (!String(constraint.expression ?? "").trim()) {
351
+ throw new ValidationError(`${constraint.name || "CHECK constraint"} needs an expression.`);
352
+ }
353
+
354
+ assertSafeSqlFragment(constraint.expression, label, { allowEmpty: false });
355
+ });
296
356
  }
297
357
 
298
358
  module.exports = {
@@ -1,4 +1,5 @@
1
1
  const { mapSqliteError, ValidationError } = require("../../utils/errors");
2
+ const { quoteIdentifier } = require("../../utils/identifier");
2
3
  const { getTableDetail } = require("./introspection");
3
4
  const { analyzeTableDesignerChanges } = require("./tableDesigner/changeAnalysis");
4
5
  const {
@@ -19,15 +20,31 @@ function getImportedFillColumns(draft) {
19
20
  );
20
21
  }
21
22
 
23
+ function getImportedRows(draft) {
24
+ return Array.isArray(draft.importRows) && draft.importRows.length
25
+ ? draft.importRows
26
+ : draft.importedCsvRows ?? [];
27
+ }
28
+
22
29
  function coerceImportedCellValue(column, value) {
23
- const textValue = String(value ?? "");
24
- const trimmedValue = textValue.trim();
30
+ if (value === null || value === undefined) {
31
+ return null;
32
+ }
33
+
34
+ const normalizedValue =
35
+ typeof value === "object" ? JSON.stringify(value) : value;
25
36
  const type = normalizeDesignerType(column.type);
37
+ const textValue = String(normalizedValue ?? "");
38
+ const trimmedValue = textValue.trim();
26
39
 
27
40
  if (!trimmedValue) {
28
41
  return ["TEXT", "DATE", "DATETIME"].includes(type) ? "" : null;
29
42
  }
30
43
 
44
+ if (typeof normalizedValue === "boolean") {
45
+ return normalizedValue ? 1 : 0;
46
+ }
47
+
31
48
  if (type === "BOOLEAN") {
32
49
  const normalized = trimmedValue.toLowerCase();
33
50
 
@@ -54,6 +71,35 @@ function coerceImportedCellValue(column, value) {
54
71
  return textValue;
55
72
  }
56
73
 
74
+ function normalizeCheckExpressionSql(expression) {
75
+ const normalized = String(expression ?? "").trim();
76
+
77
+ if (/^CHECK\s*\(/i.test(normalized)) {
78
+ return normalized;
79
+ }
80
+
81
+ return `CHECK (${normalized})`;
82
+ }
83
+
84
+ function assertSafeCheckExpressionFragment(expression) {
85
+ const normalized = String(expression ?? "").trim();
86
+
87
+ if (
88
+ normalized.includes("\0") ||
89
+ normalized.includes(";") ||
90
+ normalized.includes("--") ||
91
+ normalized.includes("/*") ||
92
+ normalized.includes("*/")
93
+ ) {
94
+ throw new ValidationError(
95
+ "CHECK expression must be a single SQL fragment without statement separators or comments.",
96
+ {
97
+ code: "CHECK_EXPRESSION_INVALID_FRAGMENT",
98
+ }
99
+ );
100
+ }
101
+ }
102
+
57
103
  class TableDesignerService {
58
104
  constructor({ connectionManager }) {
59
105
  this.connectionManager = connectionManager;
@@ -126,7 +172,9 @@ class TableDesignerService {
126
172
  const executeCreateWithImport = db.transaction((nextDraft) => {
127
173
  db.exec(buildCreateTableSql(nextDraft));
128
174
 
129
- if (!nextDraft.fillImportedRows || !nextDraft.importedCsvRows.length) {
175
+ const importRows = getImportedRows(nextDraft);
176
+
177
+ if (!nextDraft.fillImportedRows || !importRows.length) {
130
178
  return;
131
179
  }
132
180
 
@@ -143,10 +191,10 @@ class TableDesignerService {
143
191
  )
144
192
  );
145
193
 
146
- nextDraft.importedCsvRows.forEach((row) => {
194
+ importRows.forEach((row) => {
147
195
  insertStatement.run(
148
196
  importedColumns.map((column) =>
149
- coerceImportedCellValue(column, row[column.importedValueIndex] ?? "")
197
+ coerceImportedCellValue(column, row[column.importedValueIndex])
150
198
  )
151
199
  );
152
200
  });
@@ -174,6 +222,63 @@ class TableDesignerService {
174
222
  tables: listDesignerTables(db),
175
223
  };
176
224
  }
225
+
226
+ validateCheckExpression(payload = {}) {
227
+ const db = this.connectionManager.getActiveDatabase();
228
+ const draft = normalizeDraftPayload(payload.draft ?? {});
229
+ const expression = String(payload.expression ?? "").trim();
230
+
231
+ if (!expression) {
232
+ throw new ValidationError("CHECK expression is required.", {
233
+ code: "CHECK_EXPRESSION_REQUIRED",
234
+ });
235
+ }
236
+
237
+ assertSafeCheckExpressionFragment(expression);
238
+
239
+ const columns = draft.columns.filter((column) => !column.deleted && column.name);
240
+
241
+ if (!columns.length) {
242
+ throw new ValidationError("At least one named column is required to validate a CHECK expression.", {
243
+ code: "CHECK_VALIDATION_COLUMNS_REQUIRED",
244
+ });
245
+ }
246
+
247
+ const tempTableName = `__sqlite_hub_check_validation_${Date.now()}_${Math.random()
248
+ .toString(16)
249
+ .slice(2)}`;
250
+ const tempDraft = {
251
+ ...draft,
252
+ tableName: tempTableName,
253
+ columns,
254
+ checkConstraints: [
255
+ {
256
+ id: "check:validation",
257
+ expression,
258
+ },
259
+ ],
260
+ };
261
+ const createSql = buildCreateTableSql(tempDraft).replace(/^CREATE TABLE\s+/i, "CREATE TEMP TABLE ");
262
+ const dropSql = `DROP TABLE IF EXISTS ${quoteIdentifier(tempTableName)};`;
263
+
264
+ try {
265
+ db.exec(dropSql);
266
+ db.exec(createSql);
267
+ db.exec(dropSql);
268
+ return {
269
+ valid: true,
270
+ generatedSql: normalizeCheckExpressionSql(expression),
271
+ };
272
+ } catch (error) {
273
+ try {
274
+ db.exec(dropSql);
275
+ } catch {
276
+ // Ignore cleanup failures; the validation error below is the useful signal.
277
+ }
278
+
279
+ throw mapSqliteError(error);
280
+ }
281
+ }
177
282
  }
178
283
 
179
284
  module.exports = {