sqlite-hub 2.0.0 → 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 (89) hide show
  1. package/README.md +15 -10
  2. package/docs/MCP.md +36 -4
  3. package/docs/changelog.md +11 -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/settings.js +2 -2
  61. package/frontend/js/views/structure.js +28 -5
  62. package/frontend/js/views/tableAdvisor.js +14 -6
  63. package/frontend/js/views/tableDesigner.js +44 -9
  64. package/frontend/styles/components.css +676 -79
  65. package/frontend/styles/structure-graph.css +49 -0
  66. package/frontend/styles/tailwind.generated.css +1 -1
  67. package/package.json +1 -1
  68. package/server/mcp/httpRouter.js +95 -0
  69. package/server/routes/connections.js +12 -0
  70. package/server/routes/data.js +1 -1
  71. package/server/routes/settings.js +29 -7
  72. package/server/routes/tableDesigner.js +15 -1
  73. package/server/server.js +15 -0
  74. package/server/services/mcpToolService.js +28 -0
  75. package/server/services/sqlite/connectionManager.js +23 -2
  76. package/server/services/sqlite/dataBrowserService.js +21 -0
  77. package/server/services/sqlite/introspection.js +39 -1
  78. package/server/services/sqlite/structureService.js +66 -11
  79. package/server/services/sqlite/tableAdvisor.js +6 -0
  80. package/server/services/sqlite/tableDesigner/schemaMapping.js +7 -1
  81. package/server/services/sqlite/tableDesigner/sql.js +31 -2
  82. package/server/services/sqlite/tableDesigner/validation.js +69 -9
  83. package/server/services/sqlite/tableDesignerService.js +110 -5
  84. package/server/services/storage/appStateStore.js +303 -4
  85. package/frontend/assets/mockups/backups_2_create_backup_modal_1920.webp +0 -0
  86. package/frontend/assets/mockups/backups_3_edit_backup_modal_1920.webp +0 -0
  87. package/frontend/assets/mockups/backups_4_restore_backup_modal_1920.webp +0 -0
  88. package/frontend/assets/mockups/backups_5_delete_backup_modal_1920.webp +0 -0
  89. package/frontend/assets/mockups/data_3_data_export_modal_1920.webp +0 -0
@@ -76,6 +76,28 @@ const MCP_TOOL_DEFINITIONS = [
76
76
  ["databaseId", "sql"]
77
77
  ),
78
78
  },
79
+ {
80
+ name: "get_stored_queries",
81
+ description: "List saved SQL Editor queries for a database. This is the MCP equivalent of `sqlite-hub --database:name --queries`.",
82
+ inputSchema: objectSchema({
83
+ databaseId: databaseIdProperty(),
84
+ limit: { type: "integer", minimum: 1, maximum: 500, default: 100 },
85
+ }, ["databaseId"]),
86
+ },
87
+ {
88
+ name: "execute_stored_query",
89
+ description: "Execute a saved SQL Editor query by id, title, display title, or SQL fragment. This is the MCP equivalent of `sqlite-hub --database:name --execute:\"query\"`.",
90
+ inputSchema: objectSchema(
91
+ {
92
+ databaseId: databaseIdProperty(),
93
+ queryName: {
94
+ type: "string",
95
+ description: "Saved query id, title, display title, or SQL fragment.",
96
+ },
97
+ },
98
+ ["databaseId", "queryName"]
99
+ ),
100
+ },
79
101
  {
80
102
  name: "explain_query_plan",
81
103
  description: "Run SQLite EXPLAIN QUERY PLAN for a read-only query and return structured plan rows.",
@@ -227,6 +249,12 @@ class McpToolService {
227
249
  executedBy: "mcp",
228
250
  maxRows: args.maxRows,
229
251
  });
252
+ case "get_stored_queries":
253
+ return this.databaseService.listSavedQueries(args.databaseId, args.limit);
254
+ case "execute_stored_query":
255
+ return this.databaseService.executeSavedQuery(args.databaseId, args.queryName, {
256
+ executedBy: "mcp",
257
+ });
230
258
  case "explain_query_plan":
231
259
  return this.databaseService.explainQueryPlan(args.databaseId, args.sql);
232
260
  case "read_documents":
@@ -216,7 +216,7 @@ class ConnectionManager {
216
216
  return recentConnections;
217
217
  }
218
218
 
219
- updateRecentConnection(id, { filePath, label, readOnly = false, logoUpload = null, clearLogo = false }) {
219
+ updateRecentConnection(id, { filePath, label, readOnly = false, logoUpload = null, clearLogo = false, tags = null }) {
220
220
  const recentConnections = this.appStateStore.getRecentConnections();
221
221
  const existing = recentConnections.find((connection) => connection.id === id);
222
222
 
@@ -245,7 +245,7 @@ class ConnectionManager {
245
245
 
246
246
  try {
247
247
  if (this.current?.id === id) {
248
- return this.openConnection({
248
+ const connection = this.openConnection({
249
249
  filePath: resolvedPath,
250
250
  label: normalizedLabel,
251
251
  id,
@@ -253,6 +253,15 @@ class ConnectionManager {
253
253
  readOnly: normalizedReadOnly,
254
254
  logoPath: nextLogoPath,
255
255
  });
256
+
257
+ if (Array.isArray(tags)) {
258
+ this.appStateStore.setConnectionTags(id, tags);
259
+ }
260
+
261
+ return {
262
+ ...connection,
263
+ tags: this.appStateStore.getConnectionTags(id),
264
+ };
256
265
  }
257
266
 
258
267
  const db = this.openRawDatabase(resolvedPath, {
@@ -275,9 +284,14 @@ class ConnectionManager {
275
284
 
276
285
  this.appStateStore.updateRecentConnection(id, () => nextConnection);
277
286
 
287
+ if (Array.isArray(tags)) {
288
+ this.appStateStore.setConnectionTags(id, tags);
289
+ }
290
+
278
291
  return {
279
292
  ...nextConnection,
280
293
  logoUrl: this.appStateStore.getConnectionLogoUrl(nextLogoPath),
294
+ tags: this.appStateStore.getConnectionTags(id),
281
295
  isActive: false,
282
296
  };
283
297
  } catch (error) {
@@ -295,8 +309,11 @@ class ConnectionManager {
295
309
  }
296
310
 
297
311
  const { db, ...connection } = this.current;
312
+ const storedConnection = this.appStateStore.getRecentConnection(connection.id);
313
+
298
314
  return {
299
315
  ...connection,
316
+ tags: storedConnection?.tags ?? [],
300
317
  isActive: true,
301
318
  };
302
319
  }
@@ -334,6 +351,10 @@ class ConnectionManager {
334
351
  }));
335
352
  }
336
353
 
354
+ listConnectionTags() {
355
+ return this.appStateStore.listConnectionTags();
356
+ }
357
+
337
358
  getStatus() {
338
359
  const active = this.getActiveConnection();
339
360
 
@@ -83,6 +83,14 @@ function isUnchangedSubmittedValue(currentValue, submittedValue) {
83
83
  return formatPreviewValue(currentValue) === formatPreviewValue(submittedValue);
84
84
  }
85
85
 
86
+ function assertDataTableWritable(tableDetail) {
87
+ if (tableDetail.isShadow) {
88
+ throw new ValidationError(
89
+ `Shadow table ${tableDetail.name} is read-only in Data.`
90
+ );
91
+ }
92
+ }
93
+
86
94
  class DataBrowserService {
87
95
  constructor({ connectionManager }) {
88
96
  this.connectionManager = connectionManager;
@@ -102,6 +110,9 @@ class DataBrowserService {
102
110
  return {
103
111
  name: entry.name,
104
112
  columnCount,
113
+ tableKind: entry.tableKind ?? "table",
114
+ isVirtual: Boolean(entry.isVirtual),
115
+ isShadow: Boolean(entry.isShadow),
105
116
  };
106
117
  });
107
118
  }
@@ -161,6 +172,10 @@ class DataBrowserService {
161
172
  return {
162
173
  name: tableDetail.name,
163
174
  type: tableDetail.type,
175
+ tableKind: tableDetail.tableKind,
176
+ isVirtual: Boolean(tableDetail.isVirtual),
177
+ isShadow: Boolean(tableDetail.isShadow),
178
+ readOnly: Boolean(tableDetail.isShadow),
164
179
  rowCount,
165
180
  limit,
166
181
  offset,
@@ -253,6 +268,8 @@ class DataBrowserService {
253
268
  const tableDetail = getTableDetail(db, tableName, { includeRowCount: false });
254
269
  const identity = payload.identity ?? null;
255
270
 
271
+ assertDataTableWritable(tableDetail);
272
+
256
273
  if (tableDetail.notSafelyUpdatable) {
257
274
  throw new ValidationError(
258
275
  `Table ${tableName} cannot be safely updated because it has no stable row identity.`
@@ -299,6 +316,8 @@ class DataBrowserService {
299
316
  const db = this.connectionManager.getActiveDatabase();
300
317
  const tableDetail = getTableDetail(db, tableName, { includeRowCount: false });
301
318
 
319
+ assertDataTableWritable(tableDetail);
320
+
302
321
  return insertSyntheticRows(db, tableDetail, payload);
303
322
  }
304
323
 
@@ -351,6 +370,8 @@ class DataBrowserService {
351
370
  const values = payload.values ?? {};
352
371
  const identity = payload.identity ?? null;
353
372
 
373
+ assertDataTableWritable(tableDetail);
374
+
354
375
  if (tableDetail.notSafelyUpdatable) {
355
376
  throw new ValidationError(
356
377
  `Table ${tableName} cannot be safely updated because it has no stable row identity.`
@@ -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 = {