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.
Files changed (46) hide show
  1. package/README.md +14 -1
  2. package/bin/sqlite-hub-mcp.js +8 -0
  3. package/bin/sqlite-hub.js +555 -122
  4. package/docs/API.md +30 -0
  5. package/docs/CLI.md +22 -0
  6. package/docs/CLI_API_PARITY.md +61 -0
  7. package/docs/MCP.md +85 -0
  8. package/docs/changelog.md +13 -1
  9. package/docs/guidelines/AGENTS.md +32 -0
  10. package/docs/{DESIGN_GUIDELINES.md → guidelines/DESIGN.md} +10 -0
  11. package/docs/todo.md +1 -14
  12. package/frontend/js/api.js +49 -0
  13. package/frontend/js/app.js +202 -2
  14. package/frontend/js/components/connectionCard.js +3 -1
  15. package/frontend/js/components/modal.js +356 -15
  16. package/frontend/js/components/sidebar.js +41 -7
  17. package/frontend/js/components/topNav.js +2 -14
  18. package/frontend/js/router.js +10 -0
  19. package/frontend/js/store.js +483 -7
  20. package/frontend/js/utils/inputClear.js +36 -0
  21. package/frontend/js/utils/syntheticData.js +240 -0
  22. package/frontend/js/views/backups.js +52 -0
  23. package/frontend/js/views/data.js +16 -0
  24. package/frontend/js/views/logs.js +339 -0
  25. package/frontend/js/views/settings.js +266 -30
  26. package/frontend/js/views/structure.js +6 -0
  27. package/frontend/js/views/tableAdvisor.js +385 -0
  28. package/frontend/js/views/tableDesigner.js +6 -0
  29. package/frontend/styles/components.css +149 -0
  30. package/frontend/styles/tailwind.generated.css +1 -1
  31. package/frontend/styles/views.css +31 -0
  32. package/package.json +4 -2
  33. package/server/mcp/stdioServer.js +272 -0
  34. package/server/routes/data.js +45 -0
  35. package/server/routes/externalApi.js +285 -2
  36. package/server/routes/logs.js +127 -0
  37. package/server/routes/settings.js +47 -0
  38. package/server/server.js +3 -0
  39. package/server/services/databaseCommandService.js +284 -2
  40. package/server/services/mcpStatusService.js +140 -0
  41. package/server/services/mcpToolService.js +274 -0
  42. package/server/services/sqlite/dataBrowserService.js +36 -0
  43. package/server/services/sqlite/introspection.js +226 -22
  44. package/server/services/sqlite/syntheticDataGenerator.js +728 -0
  45. package/server/services/sqlite/tableAdvisor.js +790 -0
  46. 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 parseSqlStringList(text = "") {
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
- if (text[index] !== "'") {
193
+ const character = text[index];
194
+
195
+ if (/\s|,/.test(character)) {
169
196
  index += 1;
170
197
  continue;
171
198
  }
172
199
 
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
- }
200
+ if (character === "'") {
201
+ const parsed = parseSqlStringLiteral(text, index);
183
202
 
184
- index += 1;
185
- values.push(value);
186
- break;
203
+ if (!parsed) {
204
+ return [];
187
205
  }
188
206
 
189
- value += text[index];
190
- index += 1;
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 = parseSqlStringList(expression.slice(openIndex + 1, closeIndex));
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
- ...column,
342
- allowedValues: allowedValuesByColumn.get(column.name) ?? [],
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()