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.
- package/README.md +14 -9
- package/docs/MCP.md +8 -0
- package/docs/changelog.md +10 -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/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/routes/connections.js +12 -0
- package/server/routes/data.js +1 -1
- package/server/routes/tableDesigner.js +15 -1
- 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
|
@@ -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 = {
|
|
@@ -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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = {
|