sqlite-hub 1.4.0 → 2.0.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 -1
- package/bin/sqlite-hub-mcp.js +8 -0
- package/bin/sqlite-hub.js +555 -122
- package/docs/API.md +30 -0
- package/docs/CLI.md +22 -0
- package/docs/CLI_API_PARITY.md +61 -0
- package/docs/MCP.md +85 -0
- package/docs/changelog.md +13 -1
- package/docs/guidelines/AGENTS.md +32 -0
- package/docs/{DESIGN_GUIDELINES.md → guidelines/DESIGN.md} +10 -0
- package/docs/todo.md +1 -14
- package/frontend/js/api.js +49 -0
- package/frontend/js/app.js +202 -2
- package/frontend/js/components/connectionCard.js +3 -1
- package/frontend/js/components/modal.js +356 -15
- package/frontend/js/components/sidebar.js +41 -7
- package/frontend/js/components/topNav.js +2 -14
- package/frontend/js/router.js +10 -0
- package/frontend/js/store.js +483 -7
- package/frontend/js/utils/inputClear.js +36 -0
- package/frontend/js/utils/syntheticData.js +240 -0
- package/frontend/js/views/backups.js +52 -0
- package/frontend/js/views/data.js +16 -0
- package/frontend/js/views/logs.js +339 -0
- package/frontend/js/views/settings.js +266 -30
- package/frontend/js/views/structure.js +6 -0
- package/frontend/js/views/tableAdvisor.js +385 -0
- package/frontend/js/views/tableDesigner.js +6 -0
- package/frontend/styles/components.css +149 -0
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/views.css +31 -0
- package/package.json +4 -2
- package/server/mcp/stdioServer.js +272 -0
- package/server/routes/data.js +45 -0
- package/server/routes/externalApi.js +285 -2
- package/server/routes/logs.js +127 -0
- package/server/routes/settings.js +47 -0
- package/server/server.js +3 -0
- package/server/services/databaseCommandService.js +284 -2
- package/server/services/mcpStatusService.js +140 -0
- package/server/services/mcpToolService.js +274 -0
- package/server/services/sqlite/dataBrowserService.js +36 -0
- package/server/services/sqlite/introspection.js +226 -22
- package/server/services/sqlite/syntheticDataGenerator.js +728 -0
- package/server/services/sqlite/tableAdvisor.js +790 -0
- package/server/services/storage/appStateStore.js +821 -169
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
const { ValidationError } = require("../utils/errors");
|
|
2
|
+
|
|
3
|
+
function objectSchema(properties = {}, required = []) {
|
|
4
|
+
return {
|
|
5
|
+
type: "object",
|
|
6
|
+
additionalProperties: false,
|
|
7
|
+
properties,
|
|
8
|
+
required,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function databaseIdProperty() {
|
|
13
|
+
return {
|
|
14
|
+
type: "string",
|
|
15
|
+
description: "SQLite Hub database id or exact imported database label.",
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const MCP_TOOL_DEFINITIONS = [
|
|
20
|
+
{
|
|
21
|
+
name: "list_connections",
|
|
22
|
+
description: "List imported SQLite Hub database connections without exposing API tokens.",
|
|
23
|
+
inputSchema: objectSchema(),
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: "get_database_overview",
|
|
27
|
+
description: "Return operational overview, SQLite runtime metadata, and schema statistics for a database.",
|
|
28
|
+
inputSchema: objectSchema({ databaseId: databaseIdProperty() }, ["databaseId"]),
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: "list_tables",
|
|
32
|
+
description: "List tables in a database with column counts.",
|
|
33
|
+
inputSchema: objectSchema({ databaseId: databaseIdProperty() }, ["databaseId"]),
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "describe_table",
|
|
37
|
+
description: "Describe one table including columns, indexes, foreign keys, triggers, and row count.",
|
|
38
|
+
inputSchema: objectSchema(
|
|
39
|
+
{
|
|
40
|
+
databaseId: databaseIdProperty(),
|
|
41
|
+
tableName: { type: "string" },
|
|
42
|
+
},
|
|
43
|
+
["databaseId", "tableName"]
|
|
44
|
+
),
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "get_schema",
|
|
48
|
+
description: "Return the database schema: tables, views, indexes, triggers, and schema entries.",
|
|
49
|
+
inputSchema: objectSchema({ databaseId: databaseIdProperty() }, ["databaseId"]),
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "get_indexes",
|
|
53
|
+
description: "Return all indexes or indexes for one table.",
|
|
54
|
+
inputSchema: objectSchema({
|
|
55
|
+
databaseId: databaseIdProperty(),
|
|
56
|
+
tableName: { type: "string", description: "Optional table name." },
|
|
57
|
+
}, ["databaseId"]),
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "get_foreign_keys",
|
|
61
|
+
description: "Return all foreign-key metadata or foreign keys for one table.",
|
|
62
|
+
inputSchema: objectSchema({
|
|
63
|
+
databaseId: databaseIdProperty(),
|
|
64
|
+
tableName: { type: "string", description: "Optional table name." },
|
|
65
|
+
}, ["databaseId"]),
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "run_readonly_query",
|
|
69
|
+
description: "Run a read-only SELECT, PRAGMA, or EXPLAIN query. Mutating SQL is blocked server-side.",
|
|
70
|
+
inputSchema: objectSchema(
|
|
71
|
+
{
|
|
72
|
+
databaseId: databaseIdProperty(),
|
|
73
|
+
sql: { type: "string" },
|
|
74
|
+
maxRows: { type: "integer", minimum: 1, maximum: 5000, default: 500 },
|
|
75
|
+
},
|
|
76
|
+
["databaseId", "sql"]
|
|
77
|
+
),
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "explain_query_plan",
|
|
81
|
+
description: "Run SQLite EXPLAIN QUERY PLAN for a read-only query and return structured plan rows.",
|
|
82
|
+
inputSchema: objectSchema(
|
|
83
|
+
{
|
|
84
|
+
databaseId: databaseIdProperty(),
|
|
85
|
+
sql: { type: "string" },
|
|
86
|
+
},
|
|
87
|
+
["databaseId", "sql"]
|
|
88
|
+
),
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: "read_documents",
|
|
92
|
+
description: "Read database-scoped Markdown documents, optionally filtered by filename or title.",
|
|
93
|
+
inputSchema: objectSchema({
|
|
94
|
+
databaseId: databaseIdProperty(),
|
|
95
|
+
documentName: { type: "string", description: "Optional document id, filename, or title." },
|
|
96
|
+
}, ["databaseId"]),
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: "create_backup",
|
|
100
|
+
description: "Create and verify a managed SQLite Hub backup using the existing backup mechanism.",
|
|
101
|
+
inputSchema: objectSchema({
|
|
102
|
+
databaseId: databaseIdProperty(),
|
|
103
|
+
name: { type: "string" },
|
|
104
|
+
notes: { type: "string" },
|
|
105
|
+
}, ["databaseId"]),
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: "generate_types",
|
|
109
|
+
description: "Generate TypeScript, Rust, Kotlin, or Swift types from one table or all tables.",
|
|
110
|
+
inputSchema: objectSchema(
|
|
111
|
+
{
|
|
112
|
+
databaseId: databaseIdProperty(),
|
|
113
|
+
tableName: { type: "string", description: "Optional table name. Omit when allTables is true." },
|
|
114
|
+
allTables: { type: "boolean", default: false },
|
|
115
|
+
target: { type: "string", enum: ["typescript", "rust", "kotlin", "swift"] },
|
|
116
|
+
options: { type: "object", additionalProperties: true },
|
|
117
|
+
},
|
|
118
|
+
["databaseId", "target"]
|
|
119
|
+
),
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: "create_chart_from_query",
|
|
123
|
+
description: "Create a saved SQLite Hub chart from a read-only SELECT query without writing export files.",
|
|
124
|
+
inputSchema: objectSchema(
|
|
125
|
+
{
|
|
126
|
+
databaseId: databaseIdProperty(),
|
|
127
|
+
sql: { type: "string" },
|
|
128
|
+
name: { type: "string" },
|
|
129
|
+
chartType: { type: "string", enum: ["bar", "line", "pie", "scatter"] },
|
|
130
|
+
config: { type: "object", additionalProperties: true },
|
|
131
|
+
resultColumns: { type: "array", items: { type: "object", additionalProperties: true } },
|
|
132
|
+
tableVisible: { type: "boolean", default: true },
|
|
133
|
+
},
|
|
134
|
+
["databaseId", "sql", "chartType", "config"]
|
|
135
|
+
),
|
|
136
|
+
},
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
function redactConnection(connection = {}) {
|
|
140
|
+
return {
|
|
141
|
+
id: connection.id,
|
|
142
|
+
label: connection.label,
|
|
143
|
+
readOnly: Boolean(connection.readOnly),
|
|
144
|
+
sizeBytes: connection.sizeBytes ?? null,
|
|
145
|
+
lastOpenedAt: connection.lastOpenedAt ?? null,
|
|
146
|
+
lastModifiedAt: connection.lastModifiedAt ?? null,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function redactOverview(overview = {}) {
|
|
151
|
+
return {
|
|
152
|
+
...overview,
|
|
153
|
+
connection: redactConnection(overview.connection ?? {}),
|
|
154
|
+
file: {
|
|
155
|
+
filename: overview.file?.filename ?? overview.connection?.label ?? null,
|
|
156
|
+
sizeBytes: overview.file?.sizeBytes ?? null,
|
|
157
|
+
lastModifiedAt: overview.file?.lastModifiedAt ?? null,
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function summarizeBackup(backup = {}) {
|
|
163
|
+
return {
|
|
164
|
+
backupId: backup.id,
|
|
165
|
+
id: backup.id,
|
|
166
|
+
createdAt: backup.createdAt ?? null,
|
|
167
|
+
databaseId: backup.connectionId ?? null,
|
|
168
|
+
sizeBytes: backup.sizeBytes ?? null,
|
|
169
|
+
verified: backup.status === "verified",
|
|
170
|
+
status: backup.status,
|
|
171
|
+
name: backup.name,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
class McpToolService {
|
|
176
|
+
constructor({ databaseService, statusService } = {}) {
|
|
177
|
+
this.databaseService = databaseService;
|
|
178
|
+
this.statusService = statusService;
|
|
179
|
+
this.toolDefinitions = MCP_TOOL_DEFINITIONS;
|
|
180
|
+
this.toolNames = this.toolDefinitions.map((tool) => tool.name);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
listTools() {
|
|
184
|
+
return this.toolDefinitions;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
requireTool(name) {
|
|
188
|
+
const tool = this.toolDefinitions.find((definition) => definition.name === name);
|
|
189
|
+
|
|
190
|
+
if (!tool) {
|
|
191
|
+
throw new ValidationError(`Unknown MCP tool: ${name}`, { code: "MCP_TOOL_NOT_FOUND" });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return tool;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async callTool(name, args = {}) {
|
|
198
|
+
this.requireTool(name);
|
|
199
|
+
this.statusService?.markToolCall?.(name);
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
switch (name) {
|
|
203
|
+
case "list_connections":
|
|
204
|
+
return {
|
|
205
|
+
items: this.databaseService.listDatabases().map(redactConnection),
|
|
206
|
+
};
|
|
207
|
+
case "get_database_overview":
|
|
208
|
+
return redactOverview(this.databaseService.getDatabaseOverview(args.databaseId));
|
|
209
|
+
case "list_tables":
|
|
210
|
+
return {
|
|
211
|
+
items: this.databaseService.listTables(args.databaseId),
|
|
212
|
+
};
|
|
213
|
+
case "describe_table":
|
|
214
|
+
return this.databaseService.getTable(args.databaseId, args.tableName);
|
|
215
|
+
case "get_schema":
|
|
216
|
+
return this.databaseService.getSchema(args.databaseId);
|
|
217
|
+
case "get_indexes":
|
|
218
|
+
return {
|
|
219
|
+
items: this.databaseService.getIndexes(args.databaseId, args.tableName),
|
|
220
|
+
};
|
|
221
|
+
case "get_foreign_keys":
|
|
222
|
+
return {
|
|
223
|
+
items: this.databaseService.getForeignKeys(args.databaseId, args.tableName),
|
|
224
|
+
};
|
|
225
|
+
case "run_readonly_query":
|
|
226
|
+
return this.databaseService.executeReadOnlyQuery(args.databaseId, args.sql, {
|
|
227
|
+
executedBy: "mcp",
|
|
228
|
+
maxRows: args.maxRows,
|
|
229
|
+
});
|
|
230
|
+
case "explain_query_plan":
|
|
231
|
+
return this.databaseService.explainQueryPlan(args.databaseId, args.sql);
|
|
232
|
+
case "read_documents":
|
|
233
|
+
return this.databaseService.readDocuments(args.databaseId, args.documentName);
|
|
234
|
+
case "create_backup":
|
|
235
|
+
return summarizeBackup(
|
|
236
|
+
await this.databaseService.createBackup(args.databaseId, {
|
|
237
|
+
name: args.name,
|
|
238
|
+
notes: args.notes,
|
|
239
|
+
context: "mcp",
|
|
240
|
+
})
|
|
241
|
+
);
|
|
242
|
+
case "generate_types":
|
|
243
|
+
return this.databaseService.generateTypes(args.databaseId, {
|
|
244
|
+
tableName: args.tableName,
|
|
245
|
+
allTables: Boolean(args.allTables),
|
|
246
|
+
target: args.target,
|
|
247
|
+
options: args.options ?? {},
|
|
248
|
+
});
|
|
249
|
+
case "create_chart_from_query":
|
|
250
|
+
return this.databaseService.createChartFromQuery(args.databaseId, {
|
|
251
|
+
sql: args.sql,
|
|
252
|
+
name: args.name,
|
|
253
|
+
chartType: args.chartType,
|
|
254
|
+
config: args.config,
|
|
255
|
+
resultColumns: args.resultColumns,
|
|
256
|
+
tableVisible: args.tableVisible,
|
|
257
|
+
});
|
|
258
|
+
default:
|
|
259
|
+
throw new ValidationError(`Unknown MCP tool: ${name}`, { code: "MCP_TOOL_NOT_FOUND" });
|
|
260
|
+
}
|
|
261
|
+
} catch (error) {
|
|
262
|
+
this.statusService?.markError?.(error);
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
module.exports = {
|
|
269
|
+
MCP_TOOL_DEFINITIONS,
|
|
270
|
+
McpToolService,
|
|
271
|
+
redactConnection,
|
|
272
|
+
redactOverview,
|
|
273
|
+
summarizeBackup,
|
|
274
|
+
};
|
|
@@ -6,6 +6,12 @@ const {
|
|
|
6
6
|
serializeRows,
|
|
7
7
|
} = require("../../utils/sqliteTypes");
|
|
8
8
|
const { getRawStructureEntries, getTableDetail } = require("./introspection");
|
|
9
|
+
const {
|
|
10
|
+
PREVIEW_ROW_COUNT,
|
|
11
|
+
buildSyntheticRows,
|
|
12
|
+
insertSyntheticRows,
|
|
13
|
+
} = require("./syntheticDataGenerator");
|
|
14
|
+
const { analyzeTable } = require("./tableAdvisor");
|
|
9
15
|
const { normalizeTableFilter } = require("./tableFilter");
|
|
10
16
|
const { buildTableOrderClause, normalizeTableSort } = require("./tableSort");
|
|
11
17
|
|
|
@@ -272,6 +278,36 @@ class DataBrowserService {
|
|
|
272
278
|
};
|
|
273
279
|
}
|
|
274
280
|
|
|
281
|
+
previewSyntheticRows(tableName, payload = {}) {
|
|
282
|
+
const db = this.connectionManager.getActiveDatabase();
|
|
283
|
+
const tableDetail = getTableDetail(db, tableName, { includeRowCount: false });
|
|
284
|
+
const preview = buildSyntheticRows(db, tableDetail, payload, { limit: PREVIEW_ROW_COUNT });
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
tableName: tableDetail.name,
|
|
288
|
+
rowCount: preview.rowCount,
|
|
289
|
+
previewRowCount: preview.previewRowCount,
|
|
290
|
+
columns: preview.columns,
|
|
291
|
+
rows: serializeRows(preview.rows),
|
|
292
|
+
mappings: preview.mappings,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
insertSyntheticRows(tableName, payload = {}) {
|
|
297
|
+
this.connectionManager.assertWritable();
|
|
298
|
+
|
|
299
|
+
const db = this.connectionManager.getActiveDatabase();
|
|
300
|
+
const tableDetail = getTableDetail(db, tableName, { includeRowCount: false });
|
|
301
|
+
|
|
302
|
+
return insertSyntheticRows(db, tableDetail, payload);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
analyzeTable(tableName) {
|
|
306
|
+
const db = this.connectionManager.getActiveDatabase();
|
|
307
|
+
|
|
308
|
+
return analyzeTable(db, tableName);
|
|
309
|
+
}
|
|
310
|
+
|
|
275
311
|
buildWhereClause(tableDetail, identity) {
|
|
276
312
|
if (tableDetail.identityStrategy?.type === "rowid") {
|
|
277
313
|
const rowid = identity?.values?.rowid;
|
|
@@ -160,35 +160,66 @@ function normalizeIdentifier(value) {
|
|
|
160
160
|
return text.toLowerCase();
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
function
|
|
163
|
+
function parseSqlStringLiteral(text = "", startIndex = 0) {
|
|
164
|
+
let value = "";
|
|
165
|
+
let index = startIndex + 1;
|
|
166
|
+
|
|
167
|
+
while (index < text.length) {
|
|
168
|
+
if (text[index] === "'") {
|
|
169
|
+
if (text[index + 1] === "'") {
|
|
170
|
+
value += "'";
|
|
171
|
+
index += 2;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
value,
|
|
177
|
+
nextIndex: index + 1,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
value += text[index];
|
|
182
|
+
index += 1;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function parseSqlSimpleListValues(text = "") {
|
|
164
189
|
const values = [];
|
|
165
190
|
let index = 0;
|
|
166
191
|
|
|
167
192
|
while (index < text.length) {
|
|
168
|
-
|
|
193
|
+
const character = text[index];
|
|
194
|
+
|
|
195
|
+
if (/\s|,/.test(character)) {
|
|
169
196
|
index += 1;
|
|
170
197
|
continue;
|
|
171
198
|
}
|
|
172
199
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
while (index < text.length) {
|
|
177
|
-
if (text[index] === "'") {
|
|
178
|
-
if (text[index + 1] === "'") {
|
|
179
|
-
value += "'";
|
|
180
|
-
index += 2;
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
200
|
+
if (character === "'") {
|
|
201
|
+
const parsed = parseSqlStringLiteral(text, index);
|
|
183
202
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
break;
|
|
203
|
+
if (!parsed) {
|
|
204
|
+
return [];
|
|
187
205
|
}
|
|
188
206
|
|
|
189
|
-
value
|
|
190
|
-
index
|
|
207
|
+
values.push(parsed.value);
|
|
208
|
+
index = parsed.nextIndex;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const commaIndex = text.indexOf(",", index);
|
|
213
|
+
const endIndex = commaIndex === -1 ? text.length : commaIndex;
|
|
214
|
+
const token = text.slice(index, endIndex).trim();
|
|
215
|
+
const number = parseSqlNumberToken(token);
|
|
216
|
+
|
|
217
|
+
if (number === null) {
|
|
218
|
+
return [];
|
|
191
219
|
}
|
|
220
|
+
|
|
221
|
+
values.push(number);
|
|
222
|
+
index = endIndex;
|
|
192
223
|
}
|
|
193
224
|
|
|
194
225
|
return values;
|
|
@@ -219,7 +250,7 @@ function findColumnInListExpression(expression, columnName) {
|
|
|
219
250
|
continue;
|
|
220
251
|
}
|
|
221
252
|
|
|
222
|
-
const values =
|
|
253
|
+
const values = parseSqlSimpleListValues(expression.slice(openIndex + 1, closeIndex));
|
|
223
254
|
|
|
224
255
|
if (values.length) {
|
|
225
256
|
return values;
|
|
@@ -229,6 +260,137 @@ function findColumnInListExpression(expression, columnName) {
|
|
|
229
260
|
return [];
|
|
230
261
|
}
|
|
231
262
|
|
|
263
|
+
const SQL_IDENTIFIER_SOURCE =
|
|
264
|
+
'(?:"(?:[^"]|"")+"|`(?:[^`]|``)+`|\\[[^\\]]+\\]|[A-Za-z_][A-Za-z0-9_$]*)';
|
|
265
|
+
const SQL_NUMBER_SOURCE = "[+-]?(?:\\d+(?:\\.\\d+)?|\\.\\d+)(?:e[+-]?\\d+)?";
|
|
266
|
+
const SQL_NUMBER_EXACT_PATTERN = new RegExp(`^${SQL_NUMBER_SOURCE}$`, "i");
|
|
267
|
+
|
|
268
|
+
function parseSqlNumberToken(value) {
|
|
269
|
+
const text = String(value ?? "").trim();
|
|
270
|
+
|
|
271
|
+
if (!SQL_NUMBER_EXACT_PATTERN.test(text)) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const number = Number(text);
|
|
276
|
+
|
|
277
|
+
return Number.isFinite(number) ? number : null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function tokenMatchesColumn(token, columnName) {
|
|
281
|
+
return normalizeIdentifier(token) === normalizeIdentifier(columnName);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function applyIntegerLowerBound(range, value, inclusive) {
|
|
285
|
+
const min = inclusive ? Math.ceil(value) : Math.floor(value) + 1;
|
|
286
|
+
|
|
287
|
+
range.min = range.min === null ? min : Math.max(range.min, min);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function applyIntegerUpperBound(range, value, inclusive) {
|
|
291
|
+
const max = inclusive ? Math.floor(value) : Math.ceil(value) - 1;
|
|
292
|
+
|
|
293
|
+
range.max = range.max === null ? max : Math.min(range.max, max);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function applyIntegerComparison(range, operator, value) {
|
|
297
|
+
switch (operator) {
|
|
298
|
+
case ">":
|
|
299
|
+
applyIntegerLowerBound(range, value, false);
|
|
300
|
+
break;
|
|
301
|
+
case ">=":
|
|
302
|
+
applyIntegerLowerBound(range, value, true);
|
|
303
|
+
break;
|
|
304
|
+
case "<":
|
|
305
|
+
applyIntegerUpperBound(range, value, false);
|
|
306
|
+
break;
|
|
307
|
+
case "<=":
|
|
308
|
+
applyIntegerUpperBound(range, value, true);
|
|
309
|
+
break;
|
|
310
|
+
case "=":
|
|
311
|
+
applyIntegerLowerBound(range, value, true);
|
|
312
|
+
applyIntegerUpperBound(range, value, true);
|
|
313
|
+
break;
|
|
314
|
+
default:
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function reverseComparisonOperator(operator) {
|
|
320
|
+
return {
|
|
321
|
+
">": "<",
|
|
322
|
+
">=": "<=",
|
|
323
|
+
"<": ">",
|
|
324
|
+
"<=": ">=",
|
|
325
|
+
"=": "=",
|
|
326
|
+
}[operator];
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function findColumnIntegerRangeInExpression(expression, columnName) {
|
|
330
|
+
const range = { min: null, max: null };
|
|
331
|
+
let found = false;
|
|
332
|
+
const betweenPattern = new RegExp(
|
|
333
|
+
`(${SQL_IDENTIFIER_SOURCE})\\s+BETWEEN\\s+(${SQL_NUMBER_SOURCE})\\s+AND\\s+(${SQL_NUMBER_SOURCE})`,
|
|
334
|
+
"gi"
|
|
335
|
+
);
|
|
336
|
+
const comparisonPattern = new RegExp(
|
|
337
|
+
`(${SQL_IDENTIFIER_SOURCE}|${SQL_NUMBER_SOURCE})\\s*(<=|>=|=|<|>)\\s*(${SQL_IDENTIFIER_SOURCE}|${SQL_NUMBER_SOURCE})`,
|
|
338
|
+
"gi"
|
|
339
|
+
);
|
|
340
|
+
let match;
|
|
341
|
+
|
|
342
|
+
while ((match = betweenPattern.exec(expression))) {
|
|
343
|
+
const identifier = match[1];
|
|
344
|
+
const min = parseSqlNumberToken(match[2]);
|
|
345
|
+
const max = parseSqlNumberToken(match[3]);
|
|
346
|
+
|
|
347
|
+
if (!tokenMatchesColumn(identifier, columnName) || min === null || max === null) {
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
applyIntegerLowerBound(range, min, true);
|
|
352
|
+
applyIntegerUpperBound(range, max, true);
|
|
353
|
+
found = true;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
while ((match = comparisonPattern.exec(expression))) {
|
|
357
|
+
const left = match[1];
|
|
358
|
+
const operator = match[2];
|
|
359
|
+
const right = match[3];
|
|
360
|
+
const leftIsColumn = tokenMatchesColumn(left, columnName);
|
|
361
|
+
const rightIsColumn = tokenMatchesColumn(right, columnName);
|
|
362
|
+
|
|
363
|
+
if (leftIsColumn) {
|
|
364
|
+
const value = parseSqlNumberToken(right);
|
|
365
|
+
|
|
366
|
+
if (value !== null) {
|
|
367
|
+
applyIntegerComparison(range, operator, value);
|
|
368
|
+
found = true;
|
|
369
|
+
}
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (rightIsColumn) {
|
|
374
|
+
const value = parseSqlNumberToken(left);
|
|
375
|
+
const reversedOperator = reverseComparisonOperator(operator);
|
|
376
|
+
|
|
377
|
+
if (value !== null && reversedOperator) {
|
|
378
|
+
applyIntegerComparison(range, reversedOperator, value);
|
|
379
|
+
found = true;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (!found) {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
...(range.min !== null ? { min: range.min } : {}),
|
|
390
|
+
...(range.max !== null ? { max: range.max } : {}),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
232
394
|
function parseCheckAllowedValues(ddl = "", columns = []) {
|
|
233
395
|
const allowedValuesByColumn = new Map();
|
|
234
396
|
const expressions = extractCheckExpressions(ddl);
|
|
@@ -256,6 +418,42 @@ function parseCheckAllowedValues(ddl = "", columns = []) {
|
|
|
256
418
|
return allowedValuesByColumn;
|
|
257
419
|
}
|
|
258
420
|
|
|
421
|
+
function parseCheckIntegerRanges(ddl = "", columns = []) {
|
|
422
|
+
const integerRangesByColumn = new Map();
|
|
423
|
+
const expressions = extractCheckExpressions(ddl);
|
|
424
|
+
|
|
425
|
+
columns
|
|
426
|
+
.filter((column) => column.affinity === "INTEGER")
|
|
427
|
+
.forEach((column) => {
|
|
428
|
+
const range = { min: null, max: null };
|
|
429
|
+
|
|
430
|
+
expressions.forEach((expression) => {
|
|
431
|
+
const expressionRange = findColumnIntegerRangeInExpression(expression, column.name);
|
|
432
|
+
|
|
433
|
+
if (!expressionRange) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (Number.isSafeInteger(expressionRange.min)) {
|
|
438
|
+
range.min = range.min === null ? expressionRange.min : Math.max(range.min, expressionRange.min);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (Number.isSafeInteger(expressionRange.max)) {
|
|
442
|
+
range.max = range.max === null ? expressionRange.max : Math.min(range.max, expressionRange.max);
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
if (range.min !== null || range.max !== null) {
|
|
447
|
+
integerRangesByColumn.set(column.name, {
|
|
448
|
+
...(range.min !== null ? { min: range.min } : {}),
|
|
449
|
+
...(range.max !== null ? { max: range.max } : {}),
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
return integerRangesByColumn;
|
|
455
|
+
}
|
|
456
|
+
|
|
259
457
|
function groupForeignKeys(rows) {
|
|
260
458
|
const grouped = new Map();
|
|
261
459
|
|
|
@@ -336,11 +534,17 @@ function getTableDetail(db, tableName, options = {}) {
|
|
|
336
534
|
.map((column) => normalizeColumn(column, visibleSet))
|
|
337
535
|
.sort((left, right) => left.cid - right.cid);
|
|
338
536
|
const allowedValuesByColumn = parseCheckAllowedValues(entry.sql, columns);
|
|
537
|
+
const integerRangesByColumn = parseCheckIntegerRanges(entry.sql, columns);
|
|
339
538
|
|
|
340
|
-
columns = columns.map((column) =>
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
539
|
+
columns = columns.map((column) => {
|
|
540
|
+
const integerRange = integerRangesByColumn.get(column.name);
|
|
541
|
+
|
|
542
|
+
return {
|
|
543
|
+
...column,
|
|
544
|
+
allowedValues: allowedValuesByColumn.get(column.name) ?? [],
|
|
545
|
+
...(integerRange ? { integerRange } : {}),
|
|
546
|
+
};
|
|
547
|
+
});
|
|
344
548
|
|
|
345
549
|
const foreignKeys = groupForeignKeys(
|
|
346
550
|
db.prepare(`PRAGMA foreign_key_list(${quoteIdentifier(tableName)})`).all()
|