sqlite-hub 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/changelog.md +8 -0
- package/frontend/assets/mockups/connections.png +0 -0
- package/frontend/assets/mockups/data.png +0 -0
- package/frontend/assets/mockups/data_row_editor.png +0 -0
- package/frontend/assets/mockups/home.png +0 -0
- package/frontend/assets/mockups/sql_editor.png +0 -0
- package/frontend/assets/mockups/sql_editor_querydetail.png +0 -0
- package/frontend/assets/mockups/structure.png +0 -0
- package/frontend/assets/mockups/structure_inspector.png +0 -0
- package/frontend/js/api.js +15 -0
- package/frontend/js/app.js +279 -8
- package/frontend/js/components/queryEditor.js +2 -2
- package/frontend/js/components/sidebar.js +1 -0
- package/frontend/js/components/tableDesignerEditor.js +356 -0
- package/frontend/js/components/tableDesignerSidebar.js +126 -0
- package/frontend/js/components/tableDesignerSqlPreview.js +40 -0
- package/frontend/js/router.js +10 -0
- package/frontend/js/store.js +296 -1
- package/frontend/js/utils/tableDesigner.js +1192 -0
- package/frontend/js/views/data.js +253 -263
- package/frontend/js/views/tableDesigner.js +37 -0
- package/frontend/styles/base.css +87 -73
- package/frontend/styles/components.css +798 -251
- package/frontend/styles/views.css +40 -0
- package/package.json +1 -1
- package/server/routes/tableDesigner.js +60 -0
- package/server/server.js +4 -0
- package/server/services/sqlite/tableDesigner/changeAnalysis.js +295 -0
- package/server/services/sqlite/tableDesigner/schemaMapping.js +233 -0
- package/server/services/sqlite/tableDesigner/sql.js +63 -0
- package/server/services/sqlite/tableDesigner/validation.js +245 -0
- package/server/services/sqlite/tableDesignerService.js +181 -0
- package/frontend/assets/mockups/data_edit.png +0 -0
- package/frontend/assets/mockups/graph_visualize.png +0 -0
- package/frontend/assets/mockups/overview.png +0 -0
|
@@ -25,6 +25,36 @@
|
|
|
25
25
|
gap: var(--spacing-8);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
.table-designer-view {
|
|
29
|
+
display: flex;
|
|
30
|
+
height: 100%;
|
|
31
|
+
min-height: 0;
|
|
32
|
+
overflow: hidden;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.table-designer-workspace {
|
|
36
|
+
display: flex;
|
|
37
|
+
flex: 1;
|
|
38
|
+
flex-direction: column;
|
|
39
|
+
gap: var(--spacing-4);
|
|
40
|
+
min-height: 0;
|
|
41
|
+
min-width: 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.table-designer-workspace__top,
|
|
45
|
+
.table-designer-workspace__bottom {
|
|
46
|
+
min-height: 0;
|
|
47
|
+
min-width: 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.table-designer-workspace__top {
|
|
51
|
+
flex: 1;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.table-designer-workspace__bottom {
|
|
55
|
+
flex: 0 0 34%;
|
|
56
|
+
}
|
|
57
|
+
|
|
28
58
|
.landing-view {
|
|
29
59
|
align-items: center;
|
|
30
60
|
display: flex;
|
|
@@ -82,3 +112,13 @@
|
|
|
82
112
|
.system-placeholder-card.is-primary {
|
|
83
113
|
border-bottom-color: var(--color-primary-container);
|
|
84
114
|
}
|
|
115
|
+
|
|
116
|
+
@media (max-width: 1023px) {
|
|
117
|
+
.table-designer-view {
|
|
118
|
+
flex-direction: column;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.table-designer-workspace__bottom {
|
|
122
|
+
flex-basis: 280px;
|
|
123
|
+
}
|
|
124
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const express = require("express");
|
|
2
|
+
const { route, successResponse } = require("../utils/errors");
|
|
3
|
+
|
|
4
|
+
function createTableDesignerRouter({ tableDesignerService }) {
|
|
5
|
+
const router = express.Router();
|
|
6
|
+
|
|
7
|
+
router.get(
|
|
8
|
+
"/",
|
|
9
|
+
route((req, res) => {
|
|
10
|
+
const data = tableDesignerService.getOverview();
|
|
11
|
+
|
|
12
|
+
res.json(
|
|
13
|
+
successResponse({
|
|
14
|
+
data,
|
|
15
|
+
readOnly: false,
|
|
16
|
+
})
|
|
17
|
+
);
|
|
18
|
+
})
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
router.get(
|
|
22
|
+
"/:tableName",
|
|
23
|
+
route((req, res) => {
|
|
24
|
+
const data = tableDesignerService.getTableDraft(req.params.tableName);
|
|
25
|
+
|
|
26
|
+
res.json(
|
|
27
|
+
successResponse({
|
|
28
|
+
data,
|
|
29
|
+
readOnly: false,
|
|
30
|
+
})
|
|
31
|
+
);
|
|
32
|
+
})
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
router.post(
|
|
36
|
+
"/save",
|
|
37
|
+
route((req, res) => {
|
|
38
|
+
const data = tableDesignerService.saveDraft(req.body ?? {});
|
|
39
|
+
const isCreate = String(req.body?.draft?.mode ?? req.body?.mode ?? "").trim() !== "edit";
|
|
40
|
+
const fillsImportedRows = Boolean(req.body?.draft?.fillImportedRows ?? req.body?.fillImportedRows);
|
|
41
|
+
|
|
42
|
+
res.json(
|
|
43
|
+
successResponse({
|
|
44
|
+
message: isCreate
|
|
45
|
+
? fillsImportedRows
|
|
46
|
+
? "Table created and filled from CSV."
|
|
47
|
+
: "Table created."
|
|
48
|
+
: "Table schema updated.",
|
|
49
|
+
data,
|
|
50
|
+
})
|
|
51
|
+
);
|
|
52
|
+
})
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return router;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = {
|
|
59
|
+
createTableDesignerRouter,
|
|
60
|
+
};
|
package/server/server.js
CHANGED
|
@@ -11,11 +11,13 @@ const { BackupService } = require("./services/sqlite/backupService");
|
|
|
11
11
|
const { ExportService } = require("./services/sqlite/exportService");
|
|
12
12
|
const { StructureService } = require("./services/sqlite/structureService");
|
|
13
13
|
const { DataBrowserService } = require("./services/sqlite/dataBrowserService");
|
|
14
|
+
const { TableDesignerService } = require("./services/sqlite/tableDesignerService");
|
|
14
15
|
const { createConnectionsRouter } = require("./routes/connections");
|
|
15
16
|
const { createOverviewRouter } = require("./routes/overview");
|
|
16
17
|
const { createSqlRouter } = require("./routes/sql");
|
|
17
18
|
const { createStructureRouter } = require("./routes/structure");
|
|
18
19
|
const { createDataRouter } = require("./routes/data");
|
|
20
|
+
const { createTableDesignerRouter } = require("./routes/tableDesigner");
|
|
19
21
|
const { createSettingsRouter } = require("./routes/settings");
|
|
20
22
|
const { createExportRouter } = require("./routes/export");
|
|
21
23
|
|
|
@@ -46,6 +48,7 @@ const exportService = new ExportService({
|
|
|
46
48
|
});
|
|
47
49
|
const structureService = new StructureService({ connectionManager, appStateStore });
|
|
48
50
|
const dataBrowserService = new DataBrowserService({ connectionManager });
|
|
51
|
+
const tableDesignerService = new TableDesignerService({ connectionManager });
|
|
49
52
|
|
|
50
53
|
connectionManager.initialize();
|
|
51
54
|
|
|
@@ -78,6 +81,7 @@ app.use("/api/db", createOverviewRouter({ overviewService }));
|
|
|
78
81
|
app.use("/api/sql", createSqlRouter({ appStateStore, connectionManager, sqlExecutor }));
|
|
79
82
|
app.use("/api/structure", createStructureRouter({ structureService }));
|
|
80
83
|
app.use("/api/data", createDataRouter({ dataBrowserService }));
|
|
84
|
+
app.use("/api/table-designer", createTableDesignerRouter({ tableDesignerService }));
|
|
81
85
|
app.use("/api/settings", createSettingsRouter({ appStateStore }));
|
|
82
86
|
app.use("/api/export", createExportRouter({ exportService }));
|
|
83
87
|
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
const {
|
|
2
|
+
buildAlterTableAddColumnSql,
|
|
3
|
+
buildAlterTableRenameSql,
|
|
4
|
+
buildCreateTableSql,
|
|
5
|
+
} = require("./sql");
|
|
6
|
+
const { createDesignerWarning } = require("./schemaMapping");
|
|
7
|
+
const { normalizeIdentifierKey, normalizeSqlFragment } = require("./validation");
|
|
8
|
+
|
|
9
|
+
function normalizeComparableValue(value) {
|
|
10
|
+
return normalizeSqlFragment(value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function hasColumnChanged(originalColumn, draftColumn) {
|
|
14
|
+
return (
|
|
15
|
+
normalizeIdentifierKey(originalColumn.name) !== normalizeIdentifierKey(draftColumn.name) ||
|
|
16
|
+
normalizeComparableValue(originalColumn.type) !== normalizeComparableValue(draftColumn.type) ||
|
|
17
|
+
Boolean(originalColumn.notNull) !== Boolean(draftColumn.notNull) ||
|
|
18
|
+
Boolean(originalColumn.unique) !== Boolean(draftColumn.unique) ||
|
|
19
|
+
Boolean(originalColumn.primaryKey) !== Boolean(draftColumn.primaryKey) ||
|
|
20
|
+
normalizeComparableValue(originalColumn.defaultValue) !==
|
|
21
|
+
normalizeComparableValue(draftColumn.defaultValue) ||
|
|
22
|
+
normalizeIdentifierKey(originalColumn.referencesTable) !==
|
|
23
|
+
normalizeIdentifierKey(draftColumn.referencesTable) ||
|
|
24
|
+
normalizeIdentifierKey(originalColumn.referencesColumn) !==
|
|
25
|
+
normalizeIdentifierKey(draftColumn.referencesColumn)
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function hasMeaningfulNewDraftContent(draft) {
|
|
30
|
+
return (
|
|
31
|
+
Boolean(String(draft.tableName ?? "").trim()) ||
|
|
32
|
+
draft.columns.some((column) =>
|
|
33
|
+
!column.deleted &&
|
|
34
|
+
[
|
|
35
|
+
Boolean(String(column.name ?? "").trim()),
|
|
36
|
+
Boolean(String(column.defaultValue ?? "").trim()),
|
|
37
|
+
Boolean(String(column.referencesTable ?? "").trim()),
|
|
38
|
+
Boolean(String(column.referencesColumn ?? "").trim()),
|
|
39
|
+
Boolean(column.notNull),
|
|
40
|
+
Boolean(column.unique),
|
|
41
|
+
Boolean(column.primaryKey),
|
|
42
|
+
String(column.type ?? "").trim().toUpperCase() !== "TEXT",
|
|
43
|
+
].some(Boolean)
|
|
44
|
+
)
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isAlterAddColumnSafe(column) {
|
|
49
|
+
const defaultValue = normalizeComparableValue(column.defaultValue);
|
|
50
|
+
const normalizedDefault = defaultValue.toUpperCase();
|
|
51
|
+
|
|
52
|
+
if (column.primaryKey) {
|
|
53
|
+
return {
|
|
54
|
+
safe: false,
|
|
55
|
+
reason: `Column ${column.name} cannot be added with PRIMARY KEY via ALTER TABLE in SQLite.`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (column.unique) {
|
|
60
|
+
return {
|
|
61
|
+
safe: false,
|
|
62
|
+
reason: `Column ${column.name} cannot be added with UNIQUE via ALTER TABLE in SQLite.`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (column.notNull && !defaultValue) {
|
|
67
|
+
return {
|
|
68
|
+
safe: false,
|
|
69
|
+
reason:
|
|
70
|
+
`Column ${column.name} uses NOT NULL without a default value. SQLite requires a table rebuild for that change.`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (["CURRENT_TIME", "CURRENT_DATE", "CURRENT_TIMESTAMP"].includes(normalizedDefault)) {
|
|
75
|
+
return {
|
|
76
|
+
safe: false,
|
|
77
|
+
reason:
|
|
78
|
+
`Column ${column.name} uses a dynamic default value that SQLite does not allow in ALTER TABLE ADD COLUMN.`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (/^\(.+\)$/.test(defaultValue)) {
|
|
83
|
+
return {
|
|
84
|
+
safe: false,
|
|
85
|
+
reason:
|
|
86
|
+
`Column ${column.name} uses an expression default value. SQLite requires a rebuild for that ADD COLUMN operation.`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (
|
|
91
|
+
column.referencesTable &&
|
|
92
|
+
column.referencesColumn &&
|
|
93
|
+
defaultValue &&
|
|
94
|
+
normalizedDefault !== "NULL"
|
|
95
|
+
) {
|
|
96
|
+
return {
|
|
97
|
+
safe: false,
|
|
98
|
+
reason:
|
|
99
|
+
`Column ${column.name} adds a foreign key with a non-NULL default value. SQLite requires a table rebuild for that change.`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
safe: true,
|
|
105
|
+
reason: "",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function buildRiskyChangeWarning(title, message) {
|
|
110
|
+
return createDesignerWarning({
|
|
111
|
+
code: "TABLE_REBUILD_REQUIRED",
|
|
112
|
+
title,
|
|
113
|
+
message,
|
|
114
|
+
blocking: true,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function analyzeEditDraft(draft, originalDraft) {
|
|
119
|
+
const originalColumnsByName = new Map(
|
|
120
|
+
originalDraft.columns.map((column) => [normalizeIdentifierKey(column.originalName || column.name), column])
|
|
121
|
+
);
|
|
122
|
+
const matchedOriginalNames = new Set();
|
|
123
|
+
const newColumns = [];
|
|
124
|
+
const warnings = [];
|
|
125
|
+
const executableStatements = [];
|
|
126
|
+
|
|
127
|
+
draft.columns.forEach((column) => {
|
|
128
|
+
if (column.deleted) {
|
|
129
|
+
if (normalizeIdentifierKey(column.originalName || column.name)) {
|
|
130
|
+
warnings.push(
|
|
131
|
+
buildRiskyChangeWarning(
|
|
132
|
+
"Column Delete Requires Rebuild",
|
|
133
|
+
`Deleting column ${column.originalName || column.name} requires a SQLite table rebuild.`
|
|
134
|
+
)
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const originalNameKey = normalizeIdentifierKey(column.originalName);
|
|
142
|
+
|
|
143
|
+
if (originalNameKey && originalColumnsByName.has(originalNameKey)) {
|
|
144
|
+
const originalColumn = originalColumnsByName.get(originalNameKey);
|
|
145
|
+
matchedOriginalNames.add(originalNameKey);
|
|
146
|
+
|
|
147
|
+
if (hasColumnChanged(originalColumn, column)) {
|
|
148
|
+
if (normalizeIdentifierKey(originalColumn.name) !== normalizeIdentifierKey(column.name)) {
|
|
149
|
+
warnings.push(
|
|
150
|
+
buildRiskyChangeWarning(
|
|
151
|
+
"Column Rename Requires Rebuild",
|
|
152
|
+
`Renaming column ${originalColumn.name} to ${column.name} is intentionally blocked in Table Designer v1.`
|
|
153
|
+
)
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (normalizeComparableValue(originalColumn.type) !== normalizeComparableValue(column.type)) {
|
|
158
|
+
warnings.push(
|
|
159
|
+
buildRiskyChangeWarning(
|
|
160
|
+
"Column Type Change Requires Rebuild",
|
|
161
|
+
`Changing the type of ${originalColumn.name} from ${originalColumn.type} to ${column.type} requires a SQLite table rebuild.`
|
|
162
|
+
)
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (Boolean(originalColumn.notNull) !== Boolean(column.notNull)) {
|
|
167
|
+
warnings.push(
|
|
168
|
+
buildRiskyChangeWarning(
|
|
169
|
+
"NOT NULL Change Requires Rebuild",
|
|
170
|
+
`Changing the NOT NULL setting on ${originalColumn.name} requires a SQLite table rebuild.`
|
|
171
|
+
)
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (Boolean(originalColumn.unique) !== Boolean(column.unique)) {
|
|
176
|
+
warnings.push(
|
|
177
|
+
buildRiskyChangeWarning(
|
|
178
|
+
"UNIQUE Change Requires Rebuild",
|
|
179
|
+
`Changing the UNIQUE setting on ${originalColumn.name} requires a SQLite table rebuild.`
|
|
180
|
+
)
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (Boolean(originalColumn.primaryKey) !== Boolean(column.primaryKey)) {
|
|
185
|
+
warnings.push(
|
|
186
|
+
buildRiskyChangeWarning(
|
|
187
|
+
"Primary Key Change Requires Rebuild",
|
|
188
|
+
`Changing the primary key definition on ${originalColumn.name} requires a SQLite table rebuild.`
|
|
189
|
+
)
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (
|
|
194
|
+
normalizeComparableValue(originalColumn.defaultValue) !==
|
|
195
|
+
normalizeComparableValue(column.defaultValue)
|
|
196
|
+
) {
|
|
197
|
+
warnings.push(
|
|
198
|
+
buildRiskyChangeWarning(
|
|
199
|
+
"Default Value Change Requires Rebuild",
|
|
200
|
+
`Changing the default value on ${originalColumn.name} requires a SQLite table rebuild.`
|
|
201
|
+
)
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (
|
|
206
|
+
normalizeIdentifierKey(originalColumn.referencesTable) !==
|
|
207
|
+
normalizeIdentifierKey(column.referencesTable) ||
|
|
208
|
+
normalizeIdentifierKey(originalColumn.referencesColumn) !==
|
|
209
|
+
normalizeIdentifierKey(column.referencesColumn)
|
|
210
|
+
) {
|
|
211
|
+
warnings.push(
|
|
212
|
+
buildRiskyChangeWarning(
|
|
213
|
+
"Foreign Key Change Requires Rebuild",
|
|
214
|
+
`Changing the foreign key on ${originalColumn.name} requires a SQLite table rebuild.`
|
|
215
|
+
)
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
newColumns.push(column);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
originalDraft.columns.forEach((column) => {
|
|
227
|
+
const key = normalizeIdentifierKey(column.originalName || column.name);
|
|
228
|
+
const isExplicitlyDeleted = draft.columns.some(
|
|
229
|
+
(draftColumn) =>
|
|
230
|
+
draftColumn.deleted &&
|
|
231
|
+
normalizeIdentifierKey(draftColumn.originalName || draftColumn.name) === key
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
if (!matchedOriginalNames.has(key) && !isExplicitlyDeleted) {
|
|
235
|
+
warnings.push(
|
|
236
|
+
buildRiskyChangeWarning(
|
|
237
|
+
"Column Delete Requires Rebuild",
|
|
238
|
+
`Deleting column ${column.name} requires a SQLite table rebuild.`
|
|
239
|
+
)
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const tableRenameChanged =
|
|
245
|
+
normalizeIdentifierKey(draft.tableName) !== normalizeIdentifierKey(originalDraft.tableName);
|
|
246
|
+
|
|
247
|
+
if (tableRenameChanged) {
|
|
248
|
+
executableStatements.push(
|
|
249
|
+
buildAlterTableRenameSql(originalDraft.tableName, draft.tableName)
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
newColumns.forEach((column) => {
|
|
254
|
+
const addColumnSafety = isAlterAddColumnSafe(column);
|
|
255
|
+
|
|
256
|
+
if (!addColumnSafety.safe) {
|
|
257
|
+
warnings.push(
|
|
258
|
+
buildRiskyChangeWarning("Column Add Requires Rebuild", addColumnSafety.reason)
|
|
259
|
+
);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
executableStatements.push(
|
|
264
|
+
buildAlterTableAddColumnSql(tableRenameChanged ? draft.tableName : originalDraft.tableName, column)
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const dirty = Boolean(executableStatements.length || warnings.length);
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
dirty,
|
|
272
|
+
executable: warnings.length === 0,
|
|
273
|
+
statements: warnings.length === 0 ? executableStatements : [],
|
|
274
|
+
previewStatements: executableStatements,
|
|
275
|
+
warnings,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function analyzeTableDesignerChanges({ draft, originalDraft = null }) {
|
|
280
|
+
if (!originalDraft) {
|
|
281
|
+
return {
|
|
282
|
+
dirty: hasMeaningfulNewDraftContent(draft),
|
|
283
|
+
executable: true,
|
|
284
|
+
statements: [buildCreateTableSql(draft)],
|
|
285
|
+
previewStatements: [buildCreateTableSql(draft)],
|
|
286
|
+
warnings: [],
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return analyzeEditDraft(draft, originalDraft);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
module.exports = {
|
|
294
|
+
analyzeTableDesignerChanges,
|
|
295
|
+
};
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
const { quoteIdentifier } = require("../../../utils/identifier");
|
|
2
|
+
const { getRawStructureEntries } = require("../introspection");
|
|
3
|
+
|
|
4
|
+
const SUPPORTED_TABLE_DESIGNER_TYPES = [
|
|
5
|
+
"TEXT",
|
|
6
|
+
"INTEGER",
|
|
7
|
+
"REAL",
|
|
8
|
+
"BLOB",
|
|
9
|
+
"NUMERIC",
|
|
10
|
+
"BOOLEAN",
|
|
11
|
+
"DATE",
|
|
12
|
+
"DATETIME",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
function normalizeDesignerType(value) {
|
|
16
|
+
const normalized = String(value ?? "").trim().toUpperCase();
|
|
17
|
+
return normalized || "TEXT";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function createDesignerWarning({
|
|
21
|
+
code,
|
|
22
|
+
title,
|
|
23
|
+
message,
|
|
24
|
+
tone = "alert",
|
|
25
|
+
blocking = false,
|
|
26
|
+
}) {
|
|
27
|
+
return {
|
|
28
|
+
code,
|
|
29
|
+
title,
|
|
30
|
+
message,
|
|
31
|
+
tone,
|
|
32
|
+
blocking,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildSingleColumnUniqueSet(indexes = []) {
|
|
37
|
+
const uniqueColumns = new Set();
|
|
38
|
+
|
|
39
|
+
indexes.forEach((index) => {
|
|
40
|
+
if (!index?.unique || index.partial || !Array.isArray(index.columns) || index.columns.length !== 1) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const columnName = index.columns[0]?.name;
|
|
45
|
+
|
|
46
|
+
if (columnName) {
|
|
47
|
+
uniqueColumns.add(columnName);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return uniqueColumns;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function buildSimpleForeignKeyMap(foreignKeys = []) {
|
|
55
|
+
const map = new Map();
|
|
56
|
+
|
|
57
|
+
foreignKeys.forEach((foreignKey) => {
|
|
58
|
+
if (!Array.isArray(foreignKey?.mappings) || foreignKey.mappings.length !== 1) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const mapping = foreignKey.mappings[0];
|
|
63
|
+
|
|
64
|
+
if (!mapping?.from || !mapping?.to || map.has(mapping.from)) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
map.set(mapping.from, {
|
|
69
|
+
table: foreignKey.referencedTable,
|
|
70
|
+
column: mapping.to,
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return map;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function buildSchemaWarnings(tableDetail) {
|
|
78
|
+
const warnings = [];
|
|
79
|
+
const generatedColumns = (tableDetail.columns ?? []).filter((column) => column.generated);
|
|
80
|
+
const compositePrimaryKeyColumns = (tableDetail.columns ?? []).filter(
|
|
81
|
+
(column) => Number(column.primaryKeyPosition ?? 0) > 0
|
|
82
|
+
);
|
|
83
|
+
const complexForeignKeys = (tableDetail.foreignKeys ?? []).filter(
|
|
84
|
+
(foreignKey) => (foreignKey.mappings?.length ?? 0) !== 1
|
|
85
|
+
);
|
|
86
|
+
const complexUniqueIndexes = (tableDetail.indexes ?? []).filter(
|
|
87
|
+
(index) => index.unique && (index.partial || (index.columns?.length ?? 0) !== 1)
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (generatedColumns.length) {
|
|
91
|
+
warnings.push(
|
|
92
|
+
createDesignerWarning({
|
|
93
|
+
code: "GENERATED_COLUMNS_PRESENT",
|
|
94
|
+
title: "Generated Columns Detected",
|
|
95
|
+
message:
|
|
96
|
+
"Generated or hidden columns are not editable in Table Designer v1. Safe operations like table rename or adding simple columns still work.",
|
|
97
|
+
})
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (compositePrimaryKeyColumns.length > 1) {
|
|
102
|
+
warnings.push(
|
|
103
|
+
createDesignerWarning({
|
|
104
|
+
code: "COMPOSITE_PRIMARY_KEY_PRESENT",
|
|
105
|
+
title: "Composite Primary Key Detected",
|
|
106
|
+
message:
|
|
107
|
+
"This table uses more than one primary key column. Table Designer v1 preserves it, but changing primary key structure requires a manual table rebuild.",
|
|
108
|
+
})
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (complexForeignKeys.length) {
|
|
113
|
+
warnings.push(
|
|
114
|
+
createDesignerWarning({
|
|
115
|
+
code: "COMPLEX_FOREIGN_KEYS_PRESENT",
|
|
116
|
+
title: "Complex Foreign Keys Detected",
|
|
117
|
+
message:
|
|
118
|
+
"Composite or multi-mapping foreign keys cannot be edited directly in Table Designer v1. They are preserved until a rebuild is done manually.",
|
|
119
|
+
})
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (complexUniqueIndexes.length) {
|
|
124
|
+
warnings.push(
|
|
125
|
+
createDesignerWarning({
|
|
126
|
+
code: "COMPLEX_UNIQUE_CONSTRAINTS_PRESENT",
|
|
127
|
+
title: "Complex Unique Constraints Detected",
|
|
128
|
+
message:
|
|
129
|
+
"Multi-column or partial UNIQUE constraints are outside Table Designer v1. They remain untouched unless you rebuild the table manually.",
|
|
130
|
+
})
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (tableDetail.strict) {
|
|
135
|
+
warnings.push(
|
|
136
|
+
createDesignerWarning({
|
|
137
|
+
code: "STRICT_TABLE_PRESENT",
|
|
138
|
+
title: "STRICT Table",
|
|
139
|
+
message:
|
|
140
|
+
"STRICT tables can be renamed and extended with supported columns, but rebuild-style schema edits should be reviewed carefully.",
|
|
141
|
+
tone: "muted",
|
|
142
|
+
})
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (tableDetail.withoutRowId) {
|
|
147
|
+
warnings.push(
|
|
148
|
+
createDesignerWarning({
|
|
149
|
+
code: "WITHOUT_ROWID_PRESENT",
|
|
150
|
+
title: "WITHOUT ROWID Table",
|
|
151
|
+
message:
|
|
152
|
+
"WITHOUT ROWID tables can be inspected here, but rebuild-style changes are intentionally blocked in v1.",
|
|
153
|
+
tone: "muted",
|
|
154
|
+
})
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return warnings;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function mapTableColumnToDraft(column, { uniqueColumns, foreignKeyMap }) {
|
|
162
|
+
const foreignKey = foreignKeyMap.get(column.name);
|
|
163
|
+
const type = normalizeDesignerType(column.declaredType || column.affinity || "TEXT");
|
|
164
|
+
const defaultValue = column.defaultValue ?? "";
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
id: `existing:${column.cid}:${column.name}`,
|
|
168
|
+
isNew: false,
|
|
169
|
+
deleted: false,
|
|
170
|
+
name: column.name,
|
|
171
|
+
type,
|
|
172
|
+
notNull: Boolean(column.notNull),
|
|
173
|
+
unique: uniqueColumns.has(column.name),
|
|
174
|
+
primaryKey: Number(column.primaryKeyPosition ?? 0) > 0,
|
|
175
|
+
defaultValue,
|
|
176
|
+
referencesTable: foreignKey?.table ?? "",
|
|
177
|
+
referencesColumn: foreignKey?.column ?? "",
|
|
178
|
+
originalName: column.name,
|
|
179
|
+
originalType: type,
|
|
180
|
+
originalNotNull: Boolean(column.notNull),
|
|
181
|
+
originalUnique: uniqueColumns.has(column.name),
|
|
182
|
+
originalPrimaryKey: Number(column.primaryKeyPosition ?? 0) > 0,
|
|
183
|
+
originalDefaultValue: defaultValue,
|
|
184
|
+
originalReferencesTable: foreignKey?.table ?? "",
|
|
185
|
+
originalReferencesColumn: foreignKey?.column ?? "",
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function buildTableDesignerDraft(tableDetail) {
|
|
190
|
+
const uniqueColumns = buildSingleColumnUniqueSet(tableDetail.indexes);
|
|
191
|
+
const foreignKeyMap = buildSimpleForeignKeyMap(tableDetail.foreignKeys);
|
|
192
|
+
const columns = (tableDetail.columns ?? [])
|
|
193
|
+
.filter((column) => column.visible !== false && !column.generated)
|
|
194
|
+
.map((column) => mapTableColumnToDraft(column, { uniqueColumns, foreignKeyMap }));
|
|
195
|
+
const schemaWarnings = buildSchemaWarnings(tableDetail);
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
mode: "edit",
|
|
199
|
+
originalTableName: tableDetail.name,
|
|
200
|
+
tableName: tableDetail.name,
|
|
201
|
+
columns,
|
|
202
|
+
dirty: false,
|
|
203
|
+
schemaWarnings,
|
|
204
|
+
warnings: [...schemaWarnings],
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function listDesignerTables(db) {
|
|
209
|
+
return getRawStructureEntries(db)
|
|
210
|
+
.filter((entry) => entry.type === "table")
|
|
211
|
+
.map((entry) => {
|
|
212
|
+
const columns = db
|
|
213
|
+
.prepare(`PRAGMA table_xinfo(${quoteIdentifier(entry.name)})`)
|
|
214
|
+
.all()
|
|
215
|
+
.filter((column) => Number(column.hidden ?? 0) === 0)
|
|
216
|
+
.map((column) => column.name);
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
name: entry.name,
|
|
220
|
+
columnCount: columns.length,
|
|
221
|
+
columns,
|
|
222
|
+
};
|
|
223
|
+
})
|
|
224
|
+
.sort((left, right) => left.name.localeCompare(right.name, undefined, { sensitivity: "base" }));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
module.exports = {
|
|
228
|
+
SUPPORTED_TABLE_DESIGNER_TYPES,
|
|
229
|
+
buildTableDesignerDraft,
|
|
230
|
+
createDesignerWarning,
|
|
231
|
+
listDesignerTables,
|
|
232
|
+
normalizeDesignerType,
|
|
233
|
+
};
|