sqlite-hub 0.11.1 → 0.16.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 +52 -13
- package/database.sqlite +0 -0
- package/frontend/js/api.js +64 -12
- package/frontend/js/app.js +723 -37
- package/frontend/js/components/emptyState.js +42 -46
- package/frontend/js/components/modal.js +526 -2
- package/frontend/js/components/queryEditor.js +12 -3
- package/frontend/js/components/queryResults.js +79 -17
- package/frontend/js/components/rowEditorPanel.js +345 -12
- package/frontend/js/components/sidebar.js +106 -23
- package/frontend/js/components/tableDesignerEditor.js +69 -11
- package/frontend/js/store.js +437 -26
- package/frontend/js/utils/copyColumnExport.js +117 -0
- package/frontend/js/utils/exportFilenames.js +32 -0
- package/frontend/js/utils/filePathPreview.js +315 -0
- package/frontend/js/utils/format.js +1 -1
- package/frontend/js/utils/rowEditorJson.js +65 -0
- package/frontend/js/utils/sqlFormatter.js +691 -0
- package/frontend/js/utils/tableDesigner.js +178 -6
- package/frontend/js/utils/textCellStats.js +20 -0
- package/frontend/js/utils/timestampPreview.js +264 -0
- package/frontend/js/views/charts.js +3 -1
- package/frontend/js/views/data.js +39 -4
- package/frontend/js/views/editor.js +50 -1
- package/frontend/js/views/settings.js +1 -4
- package/frontend/js/views/structure.js +154 -212
- package/frontend/styles/base.css +6 -0
- package/frontend/styles/components.css +463 -2
- package/frontend/styles/structure-graph.css +0 -3
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/tokens.css +95 -95
- package/package.json +2 -3
- package/server/routes/export.js +97 -12
- package/server/services/sqlite/dataBrowserService.js +2 -68
- package/server/services/sqlite/exportService.js +74 -15
- package/server/services/sqlite/introspection.js +209 -1
- package/server/services/sqlite/sqlExecutor.js +31 -0
- package/server/services/sqlite/tableDesigner/changeAnalysis.js +25 -1
- package/server/services/sqlite/tableDesigner/schemaMapping.js +105 -10
- package/server/services/sqlite/tableDesigner/validation.js +60 -2
- package/server/services/sqlite/tableDesignerService.js +1 -1
- package/server/services/sqlite/tableFilter.js +75 -0
- package/server/utils/csv.js +30 -4
- package/tests/check-constraint-options.test.js +90 -0
- package/tests/export-filenames.test.js +34 -0
- package/tests/file-path-preview.test.js +165 -0
- package/tests/row-editor-json.test.js +82 -0
- package/tests/row-editor-timestamp-preview.test.js +192 -0
- package/tests/sql-formatter.test.js +173 -0
- package/tests/sql-highlight.test.js +38 -0
- package/tests/table-designer-v2-unique-constraints.test.js +78 -0
- package/tests/text-cell-stats.test.js +38 -0
|
@@ -6,19 +6,11 @@ const {
|
|
|
6
6
|
serializeRows,
|
|
7
7
|
} = require("../../utils/sqliteTypes");
|
|
8
8
|
const { getRawStructureEntries, getTableDetail } = require("./introspection");
|
|
9
|
+
const { normalizeTableFilter } = require("./tableFilter");
|
|
9
10
|
const { buildTableOrderClause, normalizeTableSort } = require("./tableSort");
|
|
10
11
|
|
|
11
12
|
const DEFAULT_LIMIT = 50;
|
|
12
13
|
const MAX_LIMIT = 250;
|
|
13
|
-
const FILTER_OPERATORS = new Set(["=", "!=", "<", ">", "<=", ">=", "equals"]);
|
|
14
|
-
|
|
15
|
-
function escapeLikePattern(value) {
|
|
16
|
-
return String(value).replace(/[\\%_]/g, (character) => `\\${character}`);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function isTextColumn(column) {
|
|
20
|
-
return String(column?.affinity ?? "").toUpperCase() === "TEXT";
|
|
21
|
-
}
|
|
22
14
|
|
|
23
15
|
function buildRowIdentity(tableDetail, row) {
|
|
24
16
|
if (tableDetail.identityStrategy?.type === "rowid") {
|
|
@@ -61,64 +53,6 @@ function normalizePaginationOptions(options = {}) {
|
|
|
61
53
|
};
|
|
62
54
|
}
|
|
63
55
|
|
|
64
|
-
function normalizeFilterOptions(tableDetail, options = {}) {
|
|
65
|
-
const columnName = String(options.filterColumn ?? "").trim();
|
|
66
|
-
const operator = String(options.filterOperator ?? "=").trim();
|
|
67
|
-
const value = options.filterValue;
|
|
68
|
-
|
|
69
|
-
if (!columnName || value === undefined || value === null || String(value).trim() === "") {
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (!FILTER_OPERATORS.has(operator)) {
|
|
74
|
-
throw new ValidationError(
|
|
75
|
-
`filterOperator must be one of: ${Array.from(FILTER_OPERATORS).join(", ")}.`
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const filterColumn = tableDetail.columns.find(
|
|
80
|
-
(column) => column.visible && column.name === columnName
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
if (!filterColumn) {
|
|
84
|
-
throw new ValidationError(`Unknown filter column: ${columnName}.`);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const normalizedValue = String(value);
|
|
88
|
-
const quotedColumn = quoteIdentifier(filterColumn.name);
|
|
89
|
-
|
|
90
|
-
if (operator === "equals") {
|
|
91
|
-
return {
|
|
92
|
-
column: filterColumn.name,
|
|
93
|
-
operator,
|
|
94
|
-
value: normalizedValue,
|
|
95
|
-
matchMode: "equals",
|
|
96
|
-
clause: `${quotedColumn}${isTextColumn(filterColumn) ? " COLLATE NOCASE" : ""} = ?`,
|
|
97
|
-
params: [normalizedValue],
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (isTextColumn(filterColumn) && (operator === "=" || operator === "!=")) {
|
|
102
|
-
return {
|
|
103
|
-
column: filterColumn.name,
|
|
104
|
-
operator,
|
|
105
|
-
value: normalizedValue,
|
|
106
|
-
matchMode: operator === "=" ? "contains" : "notContains",
|
|
107
|
-
clause: `${quotedColumn} COLLATE NOCASE ${operator === "=" ? "LIKE" : "NOT LIKE"} ? ESCAPE '\\'`,
|
|
108
|
-
params: [`%${escapeLikePattern(normalizedValue)}%`],
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return {
|
|
113
|
-
column: filterColumn.name,
|
|
114
|
-
operator,
|
|
115
|
-
value: normalizedValue,
|
|
116
|
-
matchMode: "comparison",
|
|
117
|
-
clause: `${quotedColumn} ${operator} ?`,
|
|
118
|
-
params: [normalizedValue],
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
56
|
function formatPreviewValue(value) {
|
|
123
57
|
if (value && typeof value === "object" && value.__type === "blob") {
|
|
124
58
|
return `BLOB ${value.sizeBytes ?? 0} bytes`;
|
|
@@ -167,7 +101,7 @@ class DataBrowserService {
|
|
|
167
101
|
const tableDetail = getTableDetail(db, tableName);
|
|
168
102
|
const { limit, offset } = normalizePaginationOptions(options);
|
|
169
103
|
const sort = normalizeTableSort(tableDetail, options);
|
|
170
|
-
const filter =
|
|
104
|
+
const filter = normalizeTableFilter(tableDetail, options);
|
|
171
105
|
const selectExpression =
|
|
172
106
|
tableDetail.identityStrategy?.type === "rowid" ? "rowid AS __rowid__, *" : "*";
|
|
173
107
|
const orderClause = buildTableOrderClause(tableDetail, sort);
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
const { quoteIdentifier } = require("../../utils/identifier");
|
|
2
2
|
const { serializeRows } = require("../../utils/sqliteTypes");
|
|
3
|
-
const {
|
|
3
|
+
const {
|
|
4
|
+
rowsToCsv,
|
|
5
|
+
rowsToDelimitedText,
|
|
6
|
+
rowsToMarkdownTable,
|
|
7
|
+
} = require("../../utils/csv");
|
|
4
8
|
const { getTableDetail } = require("./introspection");
|
|
9
|
+
const { normalizeTableFilter } = require("./tableFilter");
|
|
5
10
|
const { buildTableOrderClause, normalizeTableSort } = require("./tableSort");
|
|
6
11
|
const {
|
|
7
12
|
buildAutoTitle,
|
|
@@ -23,6 +28,43 @@ function sanitizeFilenameBase(value, fallback = "query-results") {
|
|
|
23
28
|
return sanitized.slice(0, 120);
|
|
24
29
|
}
|
|
25
30
|
|
|
31
|
+
const EXPORT_FORMATS = {
|
|
32
|
+
csv: {
|
|
33
|
+
extension: "csv",
|
|
34
|
+
mimeType: "text/csv; charset=utf-8",
|
|
35
|
+
},
|
|
36
|
+
tsv: {
|
|
37
|
+
extension: "tsv",
|
|
38
|
+
mimeType: "text/tab-separated-values; charset=utf-8",
|
|
39
|
+
},
|
|
40
|
+
md: {
|
|
41
|
+
extension: "md",
|
|
42
|
+
mimeType: "text/markdown; charset=utf-8",
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function normalizeExportFormat(format) {
|
|
47
|
+
const normalized = String(format ?? "csv").toLowerCase();
|
|
48
|
+
|
|
49
|
+
if (!EXPORT_FORMATS[normalized]) {
|
|
50
|
+
throw new Error(`Unsupported export format: ${format}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return normalized;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function renderExportContent({ columns, rows, format, csvDelimiter }) {
|
|
57
|
+
if (format === "tsv") {
|
|
58
|
+
return rowsToDelimitedText({ columns, rows, delimiter: "\t" });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (format === "md") {
|
|
62
|
+
return rowsToMarkdownTable({ columns, rows });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return rowsToCsv({ columns, rows, delimiter: csvDelimiter });
|
|
66
|
+
}
|
|
67
|
+
|
|
26
68
|
class ExportService {
|
|
27
69
|
constructor({ appStateStore, connectionManager, sqlExecutor }) {
|
|
28
70
|
this.appStateStore = appStateStore;
|
|
@@ -34,7 +76,9 @@ class ExportService {
|
|
|
34
76
|
return this.appStateStore.getSettings().csvDelimiter || ",";
|
|
35
77
|
}
|
|
36
78
|
|
|
37
|
-
exportQuery(sql) {
|
|
79
|
+
exportQuery(sql, options = {}) {
|
|
80
|
+
const format = normalizeExportFormat(options.format);
|
|
81
|
+
const formatConfig = EXPORT_FORMATS[format];
|
|
38
82
|
const activeConnection = this.connectionManager.getActiveConnection();
|
|
39
83
|
const historyItem = activeConnection
|
|
40
84
|
? this.appStateStore.findQueryHistoryItemBySql(activeConnection.id, sql)
|
|
@@ -50,44 +94,59 @@ class ExportService {
|
|
|
50
94
|
persistHistory: false,
|
|
51
95
|
requireReader: true,
|
|
52
96
|
});
|
|
97
|
+
const content = renderExportContent({
|
|
98
|
+
columns: result.columns,
|
|
99
|
+
rows: result.rows,
|
|
100
|
+
format,
|
|
101
|
+
csvDelimiter: this.getDelimiter(),
|
|
102
|
+
});
|
|
53
103
|
|
|
54
104
|
return {
|
|
55
|
-
filename: `${filenameBase}.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}),
|
|
105
|
+
filename: `${filenameBase}.${formatConfig.extension}`,
|
|
106
|
+
content,
|
|
107
|
+
csv: format === "csv" ? content : undefined,
|
|
108
|
+
format,
|
|
109
|
+
mimeType: formatConfig.mimeType,
|
|
61
110
|
columns: result.columns,
|
|
62
111
|
rowCount: result.rows.length,
|
|
63
112
|
};
|
|
64
113
|
}
|
|
65
114
|
|
|
66
115
|
exportTable(tableName, options = {}) {
|
|
116
|
+
const format = normalizeExportFormat(options.format);
|
|
117
|
+
const formatConfig = EXPORT_FORMATS[format];
|
|
67
118
|
const db = this.connectionManager.getActiveDatabase();
|
|
68
119
|
const tableDetail = getTableDetail(db, tableName, { includeRowCount: false });
|
|
69
120
|
const sort = normalizeTableSort(tableDetail, options);
|
|
121
|
+
const filter = normalizeTableFilter(tableDetail, options);
|
|
70
122
|
const orderClause = buildTableOrderClause(tableDetail, sort);
|
|
123
|
+
const whereClause = filter ? `WHERE ${filter.clause}` : "";
|
|
71
124
|
const statement = db.prepare(
|
|
72
125
|
[
|
|
73
126
|
"SELECT * FROM",
|
|
74
127
|
quoteIdentifier(tableName),
|
|
128
|
+
whereClause,
|
|
75
129
|
orderClause ? "ORDER BY" : "",
|
|
76
130
|
orderClause,
|
|
77
131
|
]
|
|
78
132
|
.filter(Boolean)
|
|
79
133
|
.join(" ")
|
|
80
134
|
);
|
|
81
|
-
const rows = serializeRows(statement.all());
|
|
135
|
+
const rows = serializeRows(statement.all(...(filter?.params ?? [])));
|
|
82
136
|
const columns = statement.columns().map((column) => column.name);
|
|
137
|
+
const content = renderExportContent({
|
|
138
|
+
columns,
|
|
139
|
+
rows,
|
|
140
|
+
format,
|
|
141
|
+
csvDelimiter: this.getDelimiter(),
|
|
142
|
+
});
|
|
83
143
|
|
|
84
144
|
return {
|
|
85
|
-
filename: `${tableName}.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}),
|
|
145
|
+
filename: `${tableName}.${formatConfig.extension}`,
|
|
146
|
+
content,
|
|
147
|
+
csv: format === "csv" ? content : undefined,
|
|
148
|
+
format,
|
|
149
|
+
mimeType: formatConfig.mimeType,
|
|
91
150
|
columns,
|
|
92
151
|
rowCount: rows.length,
|
|
93
152
|
};
|
|
@@ -64,6 +64,198 @@ function normalizeColumn(column, visibleSet) {
|
|
|
64
64
|
};
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
function isIdentifierCharacter(character) {
|
|
68
|
+
return /[A-Za-z0-9_$]/.test(character);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function skipQuotedSql(text, index) {
|
|
72
|
+
const quote = text[index];
|
|
73
|
+
let cursor = index + 1;
|
|
74
|
+
|
|
75
|
+
while (cursor < text.length) {
|
|
76
|
+
if (text[cursor] === quote) {
|
|
77
|
+
if (text[cursor + 1] === quote) {
|
|
78
|
+
cursor += 2;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return cursor + 1;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
cursor += 1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return text.length;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function findMatchingParenthesis(text, openIndex) {
|
|
92
|
+
let depth = 0;
|
|
93
|
+
|
|
94
|
+
for (let index = openIndex; index < text.length; index += 1) {
|
|
95
|
+
const character = text[index];
|
|
96
|
+
|
|
97
|
+
if (character === "'" || character === '"' || character === "`") {
|
|
98
|
+
index = skipQuotedSql(text, index) - 1;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (character === "[") {
|
|
103
|
+
const closeIndex = text.indexOf("]", index + 1);
|
|
104
|
+
index = closeIndex === -1 ? text.length : closeIndex;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (character === "(") {
|
|
109
|
+
depth += 1;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (character === ")") {
|
|
114
|
+
depth -= 1;
|
|
115
|
+
|
|
116
|
+
if (depth === 0) {
|
|
117
|
+
return index;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return -1;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function extractCheckExpressions(ddl = "") {
|
|
126
|
+
const expressions = [];
|
|
127
|
+
const checkPattern = /\bCHECK\s*\(/gi;
|
|
128
|
+
let match;
|
|
129
|
+
|
|
130
|
+
while ((match = checkPattern.exec(ddl))) {
|
|
131
|
+
const openIndex = ddl.indexOf("(", match.index);
|
|
132
|
+
const closeIndex = findMatchingParenthesis(ddl, openIndex);
|
|
133
|
+
|
|
134
|
+
if (closeIndex === -1) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
expressions.push(ddl.slice(openIndex + 1, closeIndex));
|
|
139
|
+
checkPattern.lastIndex = closeIndex + 1;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return expressions;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function normalizeIdentifier(value) {
|
|
146
|
+
const text = String(value ?? "").trim();
|
|
147
|
+
|
|
148
|
+
if (text.startsWith('"') && text.endsWith('"')) {
|
|
149
|
+
return text.slice(1, -1).replace(/""/g, '"').toLowerCase();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (text.startsWith("`") && text.endsWith("`")) {
|
|
153
|
+
return text.slice(1, -1).replace(/``/g, "`").toLowerCase();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (text.startsWith("[") && text.endsWith("]")) {
|
|
157
|
+
return text.slice(1, -1).replace(/\]\]/g, "]").toLowerCase();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return text.toLowerCase();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function parseSqlStringList(text = "") {
|
|
164
|
+
const values = [];
|
|
165
|
+
let index = 0;
|
|
166
|
+
|
|
167
|
+
while (index < text.length) {
|
|
168
|
+
if (text[index] !== "'") {
|
|
169
|
+
index += 1;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let value = "";
|
|
174
|
+
index += 1;
|
|
175
|
+
|
|
176
|
+
while (index < text.length) {
|
|
177
|
+
if (text[index] === "'") {
|
|
178
|
+
if (text[index + 1] === "'") {
|
|
179
|
+
value += "'";
|
|
180
|
+
index += 2;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
index += 1;
|
|
185
|
+
values.push(value);
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
value += text[index];
|
|
190
|
+
index += 1;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return values;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function findColumnInListExpression(expression, columnName) {
|
|
198
|
+
const normalizedColumnName = normalizeIdentifier(columnName);
|
|
199
|
+
const identifierPattern = /(?:"(?:[^"]|"")+"|`(?:[^`]|``)+`|\[[^\]]+\]|[A-Za-z_][A-Za-z0-9_$]*)\s+IN\s*\(/gi;
|
|
200
|
+
let match;
|
|
201
|
+
|
|
202
|
+
while ((match = identifierPattern.exec(expression))) {
|
|
203
|
+
const matchedIdentifier = match[0].replace(/\s+IN\s*\($/i, "").trim();
|
|
204
|
+
|
|
205
|
+
if (normalizeIdentifier(matchedIdentifier) !== normalizedColumnName) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const before = expression[match.index - 1] ?? "";
|
|
210
|
+
|
|
211
|
+
if (before && isIdentifierCharacter(before)) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const openIndex = expression.indexOf("(", match.index + matchedIdentifier.length);
|
|
216
|
+
const closeIndex = findMatchingParenthesis(expression, openIndex);
|
|
217
|
+
|
|
218
|
+
if (closeIndex === -1) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const values = parseSqlStringList(expression.slice(openIndex + 1, closeIndex));
|
|
223
|
+
|
|
224
|
+
if (values.length) {
|
|
225
|
+
return values;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return [];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function parseCheckAllowedValues(ddl = "", columns = []) {
|
|
233
|
+
const allowedValuesByColumn = new Map();
|
|
234
|
+
const expressions = extractCheckExpressions(ddl);
|
|
235
|
+
|
|
236
|
+
columns.forEach((column) => {
|
|
237
|
+
const values = [];
|
|
238
|
+
const seen = new Set();
|
|
239
|
+
|
|
240
|
+
expressions.forEach((expression) => {
|
|
241
|
+
findColumnInListExpression(expression, column.name).forEach((value) => {
|
|
242
|
+
if (seen.has(value)) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
seen.add(value);
|
|
247
|
+
values.push(value);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
if (values.length) {
|
|
252
|
+
allowedValuesByColumn.set(column.name, values);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
return allowedValuesByColumn;
|
|
257
|
+
}
|
|
258
|
+
|
|
67
259
|
function groupForeignKeys(rows) {
|
|
68
260
|
const grouped = new Map();
|
|
69
261
|
|
|
@@ -140,19 +332,33 @@ function getTableDetail(db, tableName, options = {}) {
|
|
|
140
332
|
.all();
|
|
141
333
|
const visibleSet = new Set(tableInfo.map((column) => column.name));
|
|
142
334
|
|
|
143
|
-
|
|
335
|
+
let columns = extendedInfo
|
|
144
336
|
.map((column) => normalizeColumn(column, visibleSet))
|
|
145
337
|
.sort((left, right) => left.cid - right.cid);
|
|
338
|
+
const allowedValuesByColumn = parseCheckAllowedValues(entry.sql, columns);
|
|
339
|
+
|
|
340
|
+
columns = columns.map((column) => ({
|
|
341
|
+
...column,
|
|
342
|
+
allowedValues: allowedValuesByColumn.get(column.name) ?? [],
|
|
343
|
+
}));
|
|
146
344
|
|
|
147
345
|
const foreignKeys = groupForeignKeys(
|
|
148
346
|
db.prepare(`PRAGMA foreign_key_list(${quoteIdentifier(tableName)})`).all()
|
|
149
347
|
);
|
|
348
|
+
const checkConstraints = extractCheckExpressions(entry.sql).map((expression, index) => ({
|
|
349
|
+
id: index,
|
|
350
|
+
expression: expression.trim(),
|
|
351
|
+
}));
|
|
150
352
|
|
|
151
353
|
const indexList = db
|
|
152
354
|
.prepare(`PRAGMA index_list(${quoteIdentifier(tableName)})`)
|
|
153
355
|
.all()
|
|
154
356
|
.map((indexEntry) => {
|
|
155
357
|
let indexColumns = [];
|
|
358
|
+
const indexSql =
|
|
359
|
+
db
|
|
360
|
+
.prepare("SELECT sql FROM sqlite_master WHERE type = 'index' AND name = ?")
|
|
361
|
+
.get(indexEntry.name)?.sql ?? null;
|
|
156
362
|
|
|
157
363
|
try {
|
|
158
364
|
indexColumns = db
|
|
@@ -182,6 +388,7 @@ function getTableDetail(db, tableName, options = {}) {
|
|
|
182
388
|
unique: Boolean(indexEntry.unique),
|
|
183
389
|
origin: indexEntry.origin,
|
|
184
390
|
partial: Boolean(indexEntry.partial),
|
|
391
|
+
sql: indexSql,
|
|
185
392
|
columns: indexColumns,
|
|
186
393
|
};
|
|
187
394
|
});
|
|
@@ -204,6 +411,7 @@ function getTableDetail(db, tableName, options = {}) {
|
|
|
204
411
|
withoutRowId,
|
|
205
412
|
strict: Boolean(tableListEntry?.strict),
|
|
206
413
|
columns,
|
|
414
|
+
checkConstraints,
|
|
207
415
|
foreignKeys,
|
|
208
416
|
indexes: indexList,
|
|
209
417
|
indexCount: indexList.length,
|
|
@@ -204,6 +204,13 @@ function mapEditableColumns(tableDetail, columnDefinitions) {
|
|
|
204
204
|
const tableColumnsByName = new Map(
|
|
205
205
|
tableDetail.columns.map((column) => [column.name, column])
|
|
206
206
|
);
|
|
207
|
+
const foreignKeyColumnNames = new Set(
|
|
208
|
+
(tableDetail.foreignKeys ?? []).flatMap((foreignKey) =>
|
|
209
|
+
(foreignKey.mappings ?? [])
|
|
210
|
+
.map((mapping) => String(mapping.from ?? "").trim())
|
|
211
|
+
.filter(Boolean)
|
|
212
|
+
)
|
|
213
|
+
);
|
|
207
214
|
const identityColumns = new Set(
|
|
208
215
|
tableDetail.identityStrategy?.type === "primaryKey"
|
|
209
216
|
? tableDetail.identityStrategy.columns ?? []
|
|
@@ -223,10 +230,31 @@ function mapEditableColumns(tableDetail, columnDefinitions) {
|
|
|
223
230
|
visible: isRowId ? true : Boolean(columnMeta?.visible),
|
|
224
231
|
generated: Boolean(columnMeta?.generated),
|
|
225
232
|
identity: identityColumns.has(definition.column),
|
|
233
|
+
declaredType: columnMeta?.declaredType ?? "",
|
|
234
|
+
affinity: columnMeta?.affinity ?? "",
|
|
235
|
+
primaryKeyPosition: Number(columnMeta?.primaryKeyPosition ?? 0),
|
|
236
|
+
foreignKey: foreignKeyColumnNames.has(definition.column),
|
|
237
|
+
notNull: Boolean(columnMeta?.notNull),
|
|
238
|
+
allowedValues: columnMeta?.allowedValues ?? [],
|
|
226
239
|
};
|
|
227
240
|
});
|
|
228
241
|
}
|
|
229
242
|
|
|
243
|
+
function buildTableMeta(tableDetail) {
|
|
244
|
+
return {
|
|
245
|
+
columns: (tableDetail.columns ?? []).map((column) => ({
|
|
246
|
+
name: column.name,
|
|
247
|
+
declaredType: column.declaredType,
|
|
248
|
+
affinity: column.affinity,
|
|
249
|
+
primaryKeyPosition: Number(column.primaryKeyPosition ?? 0),
|
|
250
|
+
foreignKey: (tableDetail.foreignKeys ?? []).some((foreignKey) =>
|
|
251
|
+
(foreignKey.mappings ?? []).some((mapping) => mapping.from === column.name)
|
|
252
|
+
),
|
|
253
|
+
})),
|
|
254
|
+
foreignKeys: tableDetail.foreignKeys ?? [],
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
230
258
|
function hasRequiredIdentityColumns(tableDetail, editableColumns) {
|
|
231
259
|
const availableSourceColumns = new Set(editableColumns.map((column) => column.sourceColumn));
|
|
232
260
|
|
|
@@ -299,6 +327,7 @@ function resolveEditableResult(db, columnDefinitions, serializedRows) {
|
|
|
299
327
|
reason: getEditableResultReason(tableDetail),
|
|
300
328
|
columns: editableColumns,
|
|
301
329
|
identityStrategy: tableDetail.identityStrategy,
|
|
330
|
+
tableMeta: buildTableMeta(tableDetail),
|
|
302
331
|
};
|
|
303
332
|
}
|
|
304
333
|
|
|
@@ -317,6 +346,7 @@ function resolveEditableResult(db, columnDefinitions, serializedRows) {
|
|
|
317
346
|
reason: "",
|
|
318
347
|
columns: editableColumns,
|
|
319
348
|
identityStrategy: tableDetail.identityStrategy,
|
|
349
|
+
tableMeta: buildTableMeta(tableDetail),
|
|
320
350
|
rows,
|
|
321
351
|
};
|
|
322
352
|
}
|
|
@@ -376,6 +406,7 @@ class SqlExecutor {
|
|
|
376
406
|
reason: editableResult.reason ?? "",
|
|
377
407
|
columns: editableResult.columns ?? [],
|
|
378
408
|
identityStrategy: editableResult.identityStrategy ?? null,
|
|
409
|
+
tableMeta: editableResult.tableMeta ?? null,
|
|
379
410
|
}
|
|
380
411
|
: null,
|
|
381
412
|
};
|
|
@@ -26,6 +26,17 @@ function hasColumnChanged(originalColumn, draftColumn) {
|
|
|
26
26
|
);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
function hasConstraintChanged(constraint) {
|
|
30
|
+
return (
|
|
31
|
+
normalizeComparableValue(constraint.name) !==
|
|
32
|
+
normalizeComparableValue(constraint.originalName ?? constraint.name) ||
|
|
33
|
+
normalizeComparableValue(constraint.expression || constraint.sql) !==
|
|
34
|
+
normalizeComparableValue(
|
|
35
|
+
constraint.originalExpression ?? constraint.originalSql ?? constraint.expression ?? constraint.sql
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
29
40
|
function hasMeaningfulNewDraftContent(draft) {
|
|
30
41
|
return (
|
|
31
42
|
Boolean(String(draft.tableName ?? "").trim()) ||
|
|
@@ -149,7 +160,7 @@ function analyzeEditDraft(draft, originalDraft) {
|
|
|
149
160
|
warnings.push(
|
|
150
161
|
buildRiskyChangeWarning(
|
|
151
162
|
"Column Rename Requires Rebuild",
|
|
152
|
-
`Renaming column ${originalColumn.name} to ${column.name} is intentionally blocked in Table Designer
|
|
163
|
+
`Renaming column ${originalColumn.name} to ${column.name} is intentionally blocked in Table Designer v2.`
|
|
153
164
|
)
|
|
154
165
|
);
|
|
155
166
|
}
|
|
@@ -265,6 +276,19 @@ function analyzeEditDraft(draft, originalDraft) {
|
|
|
265
276
|
);
|
|
266
277
|
});
|
|
267
278
|
|
|
279
|
+
[...(draft.uniqueConstraints ?? []), ...(draft.checkConstraints ?? [])].forEach((constraint) => {
|
|
280
|
+
if (!hasConstraintChanged(constraint)) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
warnings.push(
|
|
285
|
+
buildRiskyChangeWarning(
|
|
286
|
+
"Constraint Change Requires Rebuild",
|
|
287
|
+
`Changing ${constraint.originalName || constraint.name || "a table constraint"} requires a SQLite table rebuild.`
|
|
288
|
+
)
|
|
289
|
+
);
|
|
290
|
+
});
|
|
291
|
+
|
|
268
292
|
const dirty = Boolean(executableStatements.length || warnings.length);
|
|
269
293
|
|
|
270
294
|
return {
|