sqlite-hub 1.2.0 → 1.5.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 +113 -71
- package/bin/sqlite-hub.js +723 -116
- package/docs/API.md +152 -0
- package/docs/CLI.md +249 -0
- package/docs/CLI_API_PARITY.md +61 -0
- package/docs/changelog.md +149 -0
- package/docs/guidelines/AGENTS.md +32 -0
- package/docs/guidelines/DESIGN.md +55 -0
- package/docs/shortkeys.md +27 -0
- package/docs/todo.md +4 -0
- package/examples/api/generate-types.js +52 -0
- package/frontend/assets/mockups/backups_1_1920.webp +0 -0
- package/frontend/assets/mockups/backups_2_create_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_3_edit_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_4_restore_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_5_delete_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_1_1920.webp +0 -0
- package/frontend/assets/mockups/charts_2_query_detail_1920.webp +0 -0
- package/frontend/assets/mockups/charts_3_create_query_chart_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_4_edit_query_chart_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_5_delete_query_chart_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_6_copy_column_modal_1920.webp +0 -0
- package/frontend/assets/mockups/connections_1_1920.webp +0 -0
- package/frontend/assets/mockups/connections_2_create_connection_modal_1920.webp +0 -0
- package/frontend/assets/mockups/connections_3_open_connection_modal_1920.webp +0 -0
- package/frontend/assets/mockups/data_1_1920.webp +0 -0
- package/frontend/assets/mockups/data_2_roweditor_1920.webp +0 -0
- package/frontend/assets/mockups/data_3_data_export_modal_1920.webp +0 -0
- package/frontend/assets/mockups/documents_1_1920.webp +0 -0
- package/frontend/assets/mockups/documents_2_document_insert_table_modal_1920.webp +0 -0
- package/frontend/assets/mockups/documents_3_document_insert_note_modal_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_queue_1_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_setup_1_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_setup_2_create_media_tagging_tag_table_modal_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_setup_3_create_media_tagging_mapping_table_modal_1920.webp +0 -0
- package/frontend/assets/mockups/overview_1_1920.webp +0 -0
- package/frontend/assets/mockups/settings_1_1920.webp +0 -0
- package/frontend/assets/mockups/sql_editor_1_1920.webp +0 -0
- package/frontend/assets/mockups/sql_editor_2_query_detail_1920.webp +0 -0
- package/frontend/assets/mockups/sql_editor_3_query_export_modal_1920.webp +0 -0
- package/frontend/assets/mockups/structure_1_1920.webp +0 -0
- package/frontend/assets/mockups/structure_2_generate_types_modal_1920.webp +0 -0
- package/frontend/assets/mockups/structure_3_generate_types_modal_1920.webp +0 -0
- package/frontend/assets/mockups/table_designer_1_1920.webp +0 -0
- package/frontend/assets/mockups/table_designer_2_table_designer_constraints_modal_1920.webp +0 -0
- package/frontend/js/api.js +63 -0
- package/frontend/js/app.js +310 -10
- package/frontend/js/components/connectionCard.js +4 -2
- package/frontend/js/components/emptyState.js +4 -4
- package/frontend/js/components/generateTypesDropdown.js +33 -0
- package/frontend/js/components/metricCard.js +1 -1
- package/frontend/js/components/modal.js +555 -21
- package/frontend/js/components/pageHeader.js +1 -1
- package/frontend/js/components/queryHistoryDetail.js +3 -3
- package/frontend/js/components/queryHistoryHeader.js +1 -1
- package/frontend/js/components/queryHistoryPanel.js +1 -1
- package/frontend/js/components/rowEditorPanel.js +13 -3
- package/frontend/js/components/sidebar.js +41 -7
- package/frontend/js/components/topNav.js +2 -14
- package/frontend/js/router.js +38 -6
- package/frontend/js/store.js +870 -8
- package/frontend/js/utils/inputClear.js +36 -0
- package/frontend/js/utils/syntheticData.js +240 -0
- package/frontend/js/views/backups.js +581 -37
- package/frontend/js/views/charts.js +9 -9
- package/frontend/js/views/connections.js +2 -2
- package/frontend/js/views/data.js +17 -1
- package/frontend/js/views/documents.js +3 -3
- package/frontend/js/views/editor.js +6 -6
- package/frontend/js/views/logs.js +339 -0
- package/frontend/js/views/mediaTagging.js +7 -5
- package/frontend/js/views/overview.js +3 -3
- package/frontend/js/views/settings.js +80 -30
- package/frontend/js/views/structure.js +12 -1
- package/frontend/js/views/tableAdvisor.js +385 -0
- package/frontend/js/views/tableDesigner.js +6 -0
- package/frontend/styles/base.css +1 -40
- package/frontend/styles/components.css +258 -232
- package/frontend/styles/structure-graph.css +32 -68
- package/frontend/styles/tailwind.css +8 -4
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/tokens.css +28 -21
- package/frontend/styles/views.css +102 -261
- package/package.json +18 -2
- package/server/routes/backups.js +18 -2
- package/server/routes/data.js +45 -0
- package/server/routes/externalApi.js +321 -2
- package/server/routes/logs.js +127 -0
- package/server/routes/structure.js +22 -0
- package/server/server.js +3 -0
- package/server/services/databaseCommandService.js +59 -0
- package/server/services/sqlite/backupDiff.js +914 -0
- package/server/services/sqlite/backupService.js +77 -1
- package/server/services/sqlite/dataBrowserService.js +36 -0
- package/server/services/sqlite/introspection.js +226 -22
- package/server/services/sqlite/structureService.js +7 -0
- package/server/services/sqlite/syntheticDataGenerator.js +728 -0
- package/server/services/sqlite/tableAdvisor.js +790 -0
- package/server/services/storage/appStateStore.js +773 -169
- package/server/services/typeGenerationService.js +663 -0
- package/tailwind.config.cjs +0 -1
- package/.github/funding.yml +0 -2
- package/.github/workflows/ci.yml +0 -36
- package/database.sqlite +0 -0
- package/frontend/assets/mockups/charts_1_bars_1200.webp +0 -0
- package/frontend/assets/mockups/charts_2_pie_1200.webp +0 -0
- package/frontend/assets/mockups/charts_3_scatter_plot_1200.webp +0 -0
- package/frontend/assets/mockups/connections_1200.webp +0 -0
- package/frontend/assets/mockups/data_1_1200.webp +0 -0
- package/frontend/assets/mockups/data_2_row_editor_1200.webp +0 -0
- package/frontend/assets/mockups/documents_1200.webp +0 -0
- package/frontend/assets/mockups/media_tagging_1_setup_1200.webp +0 -0
- package/frontend/assets/mockups/media_tagging_2_tagging_queue_1200.webp +0 -0
- package/frontend/assets/mockups/media_tagging_3_media_viewer_1200.webp +0 -0
- package/frontend/assets/mockups/overview_1200.webp +0 -0
- package/frontend/assets/mockups/settings_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_1_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_2_query_details_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_3_export_column_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_4_export_column_as_markdown_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_5_export_query_result_1200.webp +0 -0
- package/frontend/assets/mockups/structure_1_1200.webp +0 -0
- package/frontend/assets/mockups/structure_2_inspector_1200.webp +0 -0
- package/frontend/assets/mockups/table_designer_1_1200.webp +0 -0
- package/frontend/assets/mockups/table_designer_2_checks_1200.webp +0 -0
- package/tests/api-token-auth.test.js +0 -279
- package/tests/backup-manager.test.js +0 -140
- package/tests/backups-view.test.js +0 -70
- package/tests/charts-height-preset-storage.test.js +0 -60
- package/tests/charts-route-state.test.js +0 -144
- package/tests/check-constraint-options.test.js +0 -90
- package/tests/cli-args.test.js +0 -113
- package/tests/cli-service-delegation.test.js +0 -140
- package/tests/connection-removal.test.js +0 -52
- package/tests/connections-file-dialog-route.test.js +0 -89
- package/tests/copy-column-modal.test.js +0 -131
- package/tests/database-command-service.test.js +0 -174
- package/tests/database-documents.test.js +0 -85
- package/tests/documents-view.test.js +0 -132
- package/tests/dropdown-button.test.js +0 -101
- package/tests/email-preview.test.js +0 -89
- package/tests/export-blob.test.js +0 -99
- package/tests/export-filenames.test.js +0 -38
- package/tests/file-path-preview.test.js +0 -165
- package/tests/form-controls.test.js +0 -34
- package/tests/json-preview.test.js +0 -49
- package/tests/local-request-security.test.js +0 -85
- package/tests/markdown-documents.test.js +0 -103
- package/tests/native-file-dialog.test.js +0 -105
- package/tests/query-editor.test.js +0 -28
- package/tests/query-history-detail.test.js +0 -37
- package/tests/query-history-header.test.js +0 -30
- package/tests/query-results-truncation.test.js +0 -20
- package/tests/risky-sql.test.js +0 -30
- package/tests/row-editor-json.test.js +0 -82
- package/tests/row-editor-null-values.test.js +0 -184
- package/tests/row-editor-timestamp-preview.test.js +0 -192
- package/tests/security-paths.test.js +0 -84
- package/tests/settings-api-tokens-route.test.js +0 -97
- package/tests/settings-metadata.test.js +0 -114
- package/tests/settings-view.test.js +0 -108
- package/tests/sql-formatter.test.js +0 -173
- package/tests/sql-highlight.test.js +0 -38
- package/tests/sql-identifier-safety.test.js +0 -171
- package/tests/sql-result-limit.test.js +0 -66
- package/tests/structure-view.test.js +0 -60
- package/tests/table-designer-v2-unique-constraints.test.js +0 -78
- package/tests/table-scroll-state.test.js +0 -70
- package/tests/text-cell-stats.test.js +0 -38
|
@@ -19,6 +19,10 @@ const {
|
|
|
19
19
|
validateSqlitePath,
|
|
20
20
|
} = require("../../utils/fileValidation");
|
|
21
21
|
const { quoteIdentifier } = require("../../utils/identifier");
|
|
22
|
+
const {
|
|
23
|
+
buildBackupDiff,
|
|
24
|
+
normalizeBackupDiffSampleLimit,
|
|
25
|
+
} = require("./backupDiff");
|
|
22
26
|
|
|
23
27
|
const BACKUP_TYPES = new Set([
|
|
24
28
|
"manual",
|
|
@@ -161,8 +165,9 @@ class BackupService {
|
|
|
161
165
|
return this.decorateBackupFileState(backup);
|
|
162
166
|
}
|
|
163
167
|
|
|
164
|
-
|
|
168
|
+
updateBackupDetails(backupId, { name, notes } = {}) {
|
|
165
169
|
const backup = this.appStateStore.updateBackupRecord(backupId, {
|
|
170
|
+
name,
|
|
166
171
|
notes,
|
|
167
172
|
});
|
|
168
173
|
|
|
@@ -393,6 +398,77 @@ class BackupService {
|
|
|
393
398
|
};
|
|
394
399
|
}
|
|
395
400
|
|
|
401
|
+
backupBelongsToConnection(backup, connection) {
|
|
402
|
+
if (!backup || !connection) {
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (backup.connectionId && backup.connectionId === connection.id) {
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (backup.sourcePath && connection.path) {
|
|
411
|
+
return path.resolve(backup.sourcePath) === path.resolve(connection.path);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
diffBackupWithCurrent(backupId, { sampleLimit } = {}) {
|
|
418
|
+
const normalizedSampleLimit = normalizeBackupDiffSampleLimit(sampleLimit);
|
|
419
|
+
const activeConnection = this.connectionManager.getActiveConnection();
|
|
420
|
+
|
|
421
|
+
if (!activeConnection) {
|
|
422
|
+
throw new DatabaseRequiredError("No active SQLite database selected for backup comparison.");
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (this.activeOperations.size > 0) {
|
|
426
|
+
throw new ConflictError("A backup operation is already running.");
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const backup = this.getBackup(backupId);
|
|
430
|
+
|
|
431
|
+
if (backup.status !== "verified") {
|
|
432
|
+
throw new ValidationError("Only verified backups can be compared.");
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (!backup.fileExists) {
|
|
436
|
+
throw new NotFoundError("Backup file is missing.");
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (!this.backupBelongsToConnection(backup, activeConnection)) {
|
|
440
|
+
throw new ValidationError("Backup does not belong to the active database.");
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
let backupDb = null;
|
|
444
|
+
|
|
445
|
+
try {
|
|
446
|
+
backupDb = new Database(backup.path, {
|
|
447
|
+
readonly: true,
|
|
448
|
+
fileMustExist: true,
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
const quickCheck = readQuickCheckResult(backupDb);
|
|
452
|
+
|
|
453
|
+
if (quickCheck !== "ok") {
|
|
454
|
+
throw new ValidationError(`Backup verification failed: ${quickCheck}`);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return buildBackupDiff({
|
|
458
|
+
backupDb,
|
|
459
|
+
currentDb: this.connectionManager.getActiveDatabase(),
|
|
460
|
+
backup,
|
|
461
|
+
currentConnection: activeConnection,
|
|
462
|
+
comparedAt: new Date(),
|
|
463
|
+
sampleLimit: normalizedSampleLimit,
|
|
464
|
+
});
|
|
465
|
+
} catch (error) {
|
|
466
|
+
throw mapSqliteError(error);
|
|
467
|
+
} finally {
|
|
468
|
+
backupDb?.close();
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
396
472
|
deleteBackup(backupId) {
|
|
397
473
|
const backup = this.getBackup(backupId);
|
|
398
474
|
|
|
@@ -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()
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
const { getRawStructureEntries, getTableDetail } = require("./introspection");
|
|
2
2
|
const { quoteIdentifier } = require("../../utils/identifier");
|
|
3
3
|
const { serializeRows } = require("../../utils/sqliteTypes");
|
|
4
|
+
const { TypeGenerationService } = require("../typeGenerationService");
|
|
4
5
|
|
|
5
6
|
class StructureService {
|
|
6
7
|
constructor({ connectionManager, appStateStore }) {
|
|
7
8
|
this.connectionManager = connectionManager;
|
|
8
9
|
this.appStateStore = appStateStore;
|
|
10
|
+
this.typeGenerationService = new TypeGenerationService();
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
getStructureOverview() {
|
|
@@ -79,6 +81,11 @@ class StructureService {
|
|
|
79
81
|
notSafelyUpdatable: table.notSafelyUpdatable,
|
|
80
82
|
};
|
|
81
83
|
}
|
|
84
|
+
|
|
85
|
+
generateTableTypes(tableName, target, options = {}) {
|
|
86
|
+
const db = this.connectionManager.getActiveDatabase();
|
|
87
|
+
return this.typeGenerationService.generateTypesFromDatabase(db, tableName, target, options);
|
|
88
|
+
}
|
|
82
89
|
}
|
|
83
90
|
|
|
84
91
|
module.exports = {
|