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.
- package/README.md +15 -10
- package/docs/MCP.md +36 -4
- package/docs/changelog.md +11 -0
- package/docs/guidelines/DESIGN.md +64 -0
- package/frontend/assets/mockups/backups_1_1920.webp +0 -0
- package/frontend/assets/mockups/backups_2_compare_drawer_1920.webp +0 -0
- package/frontend/assets/mockups/backups_3_create_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_4_edit_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_5_restore_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_6_delete_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_1_1920.webp +0 -0
- package/frontend/assets/mockups/charts_2_query_detail_1920.webp +0 -0
- package/frontend/assets/mockups/charts_3_create_query_chart_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_4_edit_query_chart_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_5_delete_query_chart_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_6_copy_column_modal_1920.webp +0 -0
- package/frontend/assets/mockups/connections_1_1920.webp +0 -0
- package/frontend/assets/mockups/connections_2_create_connection_modal_1920.webp +0 -0
- package/frontend/assets/mockups/connections_3_open_connection_modal_1920.webp +0 -0
- package/frontend/assets/mockups/data_1_1920.webp +0 -0
- package/frontend/assets/mockups/data_2_roweditor_1920.webp +0 -0
- package/frontend/assets/mockups/data_3_generate_data_modal_1920.webp +0 -0
- package/frontend/assets/mockups/data_4_data_export_modal_1920.webp +0 -0
- package/frontend/assets/mockups/documents_1_1920.webp +0 -0
- package/frontend/assets/mockups/documents_2_document_insert_table_modal_1920.webp +0 -0
- package/frontend/assets/mockups/documents_3_document_insert_note_modal_1920.webp +0 -0
- package/frontend/assets/mockups/logs_1_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_queue_1_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_setup_1_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_setup_2_create_media_tagging_tag_table_modal_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_setup_3_create_media_tagging_mapping_table_modal_1920.webp +0 -0
- package/frontend/assets/mockups/overview_1_1920.webp +0 -0
- package/frontend/assets/mockups/settings_1_1920.webp +0 -0
- package/frontend/assets/mockups/sql_editor_1_1920.webp +0 -0
- package/frontend/assets/mockups/sql_editor_2_query_detail_1920.webp +0 -0
- package/frontend/assets/mockups/sql_editor_3_query_export_modal_1920.webp +0 -0
- package/frontend/assets/mockups/structure_1_1920.webp +0 -0
- package/frontend/assets/mockups/structure_2_generate_types_modal_1920.webp +0 -0
- package/frontend/assets/mockups/structure_3_generate_types_modal_1920.webp +0 -0
- package/frontend/assets/mockups/table_designer_1_1920.webp +0 -0
- package/frontend/assets/mockups/table_designer_2_table_designer_constraints_modal_1920.webp +0 -0
- package/frontend/js/api.js +11 -0
- package/frontend/js/app.js +238 -8
- package/frontend/js/components/badges.js +16 -0
- package/frontend/js/components/connectionCard.js +35 -0
- package/frontend/js/components/modal.js +116 -245
- package/frontend/js/components/queryHistoryDetail.js +25 -45
- package/frontend/js/components/rowEditorPanel.js +7 -60
- package/frontend/js/components/structureGraph.js +292 -13
- package/frontend/js/components/tableDesignerConstraintsDrawer.js +400 -0
- package/frontend/js/components/tableDesignerEditor.js +51 -16
- package/frontend/js/components/tableDesignerSidebar.js +9 -5
- package/frontend/js/store.js +551 -7
- package/frontend/js/utils/connectionRegistry.js +76 -0
- package/frontend/js/utils/tableDesigner.js +431 -62
- package/frontend/js/views/backups.js +2 -5
- package/frontend/js/views/charts.js +20 -10
- package/frontend/js/views/connections.js +103 -3
- package/frontend/js/views/data.js +19 -5
- package/frontend/js/views/settings.js +2 -2
- package/frontend/js/views/structure.js +28 -5
- package/frontend/js/views/tableAdvisor.js +14 -6
- package/frontend/js/views/tableDesigner.js +44 -9
- package/frontend/styles/components.css +676 -79
- package/frontend/styles/structure-graph.css +49 -0
- package/frontend/styles/tailwind.generated.css +1 -1
- package/package.json +1 -1
- package/server/mcp/httpRouter.js +95 -0
- package/server/routes/connections.js +12 -0
- package/server/routes/data.js +1 -1
- package/server/routes/settings.js +29 -7
- package/server/routes/tableDesigner.js +15 -1
- package/server/server.js +15 -0
- package/server/services/mcpToolService.js +28 -0
- package/server/services/sqlite/connectionManager.js +23 -2
- package/server/services/sqlite/dataBrowserService.js +21 -0
- package/server/services/sqlite/introspection.js +39 -1
- package/server/services/sqlite/structureService.js +66 -11
- package/server/services/sqlite/tableAdvisor.js +6 -0
- package/server/services/sqlite/tableDesigner/schemaMapping.js +7 -1
- package/server/services/sqlite/tableDesigner/sql.js +31 -2
- package/server/services/sqlite/tableDesigner/validation.js +69 -9
- package/server/services/sqlite/tableDesignerService.js +110 -5
- package/server/services/storage/appStateStore.js +303 -4
- package/frontend/assets/mockups/backups_2_create_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_3_edit_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_4_restore_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_5_delete_backup_modal_1920.webp +0 -0
- 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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
|
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${
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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.
|
|
294
|
-
throw new ValidationError("Fill requires imported
|
|
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 = {
|