sqlite-hub 1.1.2 → 1.4.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 +106 -73
- package/bin/sqlite-hub.js +176 -2
- package/docs/API.md +122 -0
- package/docs/CLI.md +227 -0
- package/docs/DESIGN_GUIDELINES.md +45 -0
- package/docs/changelog.md +141 -0
- package/docs/shortkeys.md +27 -0
- package/docs/todo.md +16 -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 +18 -0
- package/frontend/js/app.js +209 -13
- package/frontend/js/components/connectionCard.js +1 -1
- 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 +211 -18
- 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/queryResults.js +1 -1
- package/frontend/js/components/rowEditorPanel.js +65 -16
- package/frontend/js/components/topNav.js +1 -4
- package/frontend/js/components/workspaceOpenDropdown.js +52 -0
- package/frontend/js/router.js +28 -6
- package/frontend/js/store.js +489 -9
- package/frontend/js/utils/emailPreview.js +28 -0
- package/frontend/js/utils/markdownDocuments.js +17 -1
- package/frontend/js/views/backups.js +556 -36
- package/frontend/js/views/charts.js +9 -9
- package/frontend/js/views/connections.js +2 -2
- package/frontend/js/views/data.js +43 -17
- package/frontend/js/views/documents.js +3 -3
- package/frontend/js/views/editor.js +6 -6
- package/frontend/js/views/mediaTagging.js +7 -5
- package/frontend/js/views/overview.js +3 -3
- package/frontend/js/views/settings.js +4 -3
- package/frontend/js/views/structure.js +27 -10
- package/frontend/js/views/tableDesigner.js +23 -0
- package/frontend/styles/base.css +1 -40
- package/frontend/styles/components.css +114 -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 +29 -21
- package/frontend/styles/views.css +94 -261
- package/package.json +18 -2
- package/server/routes/backups.js +18 -2
- package/server/routes/externalApi.js +36 -0
- package/server/routes/structure.js +22 -0
- package/server/services/databaseCommandService.js +14 -0
- package/server/services/sqlite/backupDiff.js +914 -0
- package/server/services/sqlite/backupService.js +77 -1
- package/server/services/sqlite/structureService.js +7 -0
- 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 -64
- 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 -75
- 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 -79
- 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 -155
- 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 -107
- 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 -56
- 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
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
const assert = require("node:assert/strict");
|
|
2
|
-
const test = require("node:test");
|
|
3
|
-
|
|
4
|
-
const { version } = require("../package.json");
|
|
5
|
-
const {
|
|
6
|
-
checkLatestAppVersion,
|
|
7
|
-
compareSemver,
|
|
8
|
-
isNewerVersion,
|
|
9
|
-
readSettingsMetadata,
|
|
10
|
-
readSqliteVersion,
|
|
11
|
-
} = require("../server/routes/settings");
|
|
12
|
-
const { buildAppInfo } = require("../server/services/appInfoService");
|
|
13
|
-
|
|
14
|
-
test("settings metadata exposes app and SQLite versions", () => {
|
|
15
|
-
const metadata = readSettingsMetadata();
|
|
16
|
-
|
|
17
|
-
assert.equal(metadata.appVersion, version);
|
|
18
|
-
assert.match(metadata.sqliteVersion, /^\d+\.\d+\.\d+$/);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
test("SQLite runtime version is readable", () => {
|
|
22
|
-
assert.match(readSqliteVersion(), /^\d+\.\d+\.\d+$/);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
test("settings version comparison detects newer releases", () => {
|
|
26
|
-
assert.equal(compareSemver("1.2.0", "1.1.9"), 1);
|
|
27
|
-
assert.equal(compareSemver("1.0.1", "1.0.1"), 0);
|
|
28
|
-
assert.equal(compareSemver("1.0.0", "1.0.1"), -1);
|
|
29
|
-
assert.equal(isNewerVersion("1.0.2", "1.0.1"), true);
|
|
30
|
-
assert.equal(isNewerVersion("1.0.1", "1.0.1"), false);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
test("settings version check reads the npm latest version", async () => {
|
|
34
|
-
const result = await checkLatestAppVersion({
|
|
35
|
-
currentVersion: "1.0.1",
|
|
36
|
-
packageName: "sqlite-hub",
|
|
37
|
-
fetchImpl: async (url, options) => {
|
|
38
|
-
assert.equal(url, "https://registry.npmjs.org/sqlite-hub/latest");
|
|
39
|
-
assert.equal(options.headers.Accept, "application/json");
|
|
40
|
-
|
|
41
|
-
return {
|
|
42
|
-
ok: true,
|
|
43
|
-
json: async () => ({ version: "1.1.0" }),
|
|
44
|
-
};
|
|
45
|
-
},
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
assert.equal(result.packageName, "sqlite-hub");
|
|
49
|
-
assert.equal(result.currentVersion, "1.0.1");
|
|
50
|
-
assert.equal(result.latestVersion, "1.1.0");
|
|
51
|
-
assert.equal(result.updateAvailable, true);
|
|
52
|
-
assert.equal(result.source, "npm");
|
|
53
|
-
assert.equal(result.releaseUrl, "https://www.npmjs.com/package/sqlite-hub/v/1.1.0");
|
|
54
|
-
assert.match(result.checkedAt, /^\d{4}-\d{2}-\d{2}T/);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test("app info includes update status for CLI and API consumers", async () => {
|
|
58
|
-
const info = await buildAppInfo({
|
|
59
|
-
port: 4173,
|
|
60
|
-
url: "http://127.0.0.1:4173",
|
|
61
|
-
currentVersion: "1.0.1",
|
|
62
|
-
sqliteVersion: "3.50.0",
|
|
63
|
-
versionCheckService: async () => ({
|
|
64
|
-
packageName: "sqlite-hub",
|
|
65
|
-
currentVersion: "1.0.1",
|
|
66
|
-
latestVersion: "1.1.0",
|
|
67
|
-
updateAvailable: true,
|
|
68
|
-
checkedAt: "2026-06-20T10:00:00.000Z",
|
|
69
|
-
source: "npm",
|
|
70
|
-
releaseUrl: "https://www.npmjs.com/package/sqlite-hub/v/1.1.0",
|
|
71
|
-
}),
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
assert.equal(info.packageName, "sqlite-hub");
|
|
75
|
-
assert.equal(info.appVersion, "1.0.1");
|
|
76
|
-
assert.equal(info.sqliteVersion, "3.50.0");
|
|
77
|
-
assert.equal(info.port, 4173);
|
|
78
|
-
assert.equal(info.url, "http://127.0.0.1:4173");
|
|
79
|
-
assert.equal(info.versionCheck.status, "update_available");
|
|
80
|
-
assert.equal(info.versionCheck.updateAvailable, true);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
test("app info reports unknown version status when registry lookup fails", async () => {
|
|
84
|
-
const info = await buildAppInfo({
|
|
85
|
-
currentVersion: "1.0.1",
|
|
86
|
-
sqliteVersion: "3.50.0",
|
|
87
|
-
versionCheckService: async () => {
|
|
88
|
-
throw new Error("offline");
|
|
89
|
-
},
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
assert.equal(info.versionCheck.status, "unknown");
|
|
93
|
-
assert.equal(info.versionCheck.updateAvailable, null);
|
|
94
|
-
assert.equal(info.versionCheck.error.message, "offline");
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test("app info marks local builds newer than npm latest as ahead", async () => {
|
|
98
|
-
const info = await buildAppInfo({
|
|
99
|
-
currentVersion: "1.1.0",
|
|
100
|
-
sqliteVersion: "3.50.0",
|
|
101
|
-
versionCheckService: async () => ({
|
|
102
|
-
packageName: "sqlite-hub",
|
|
103
|
-
currentVersion: "1.1.0",
|
|
104
|
-
latestVersion: "1.0.0",
|
|
105
|
-
updateAvailable: false,
|
|
106
|
-
checkedAt: "2026-06-20T10:00:00.000Z",
|
|
107
|
-
source: "npm",
|
|
108
|
-
releaseUrl: "https://www.npmjs.com/package/sqlite-hub/v/1.0.0",
|
|
109
|
-
}),
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
assert.equal(info.versionCheck.status, "ahead");
|
|
113
|
-
assert.equal(info.versionCheck.updateAvailable, false);
|
|
114
|
-
});
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
const assert = require("node:assert/strict");
|
|
2
|
-
const path = require("node:path");
|
|
3
|
-
const { pathToFileURL } = require("node:url");
|
|
4
|
-
const test = require("node:test");
|
|
5
|
-
|
|
6
|
-
let settingsViewModulePromise = null;
|
|
7
|
-
|
|
8
|
-
function loadSettingsViewModule() {
|
|
9
|
-
if (!settingsViewModulePromise) {
|
|
10
|
-
settingsViewModulePromise = import(
|
|
11
|
-
pathToFileURL(path.resolve(__dirname, "../frontend/js/views/settings.js")).href
|
|
12
|
-
);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
return settingsViewModulePromise;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
test("settings view shows SQLite runtime version and custom port command", async () => {
|
|
19
|
-
const { renderSettingsView } = await loadSettingsViewModule();
|
|
20
|
-
const rendered = renderSettingsView({
|
|
21
|
-
settings: {
|
|
22
|
-
loading: false,
|
|
23
|
-
error: null,
|
|
24
|
-
appVersion: "1.2.3",
|
|
25
|
-
sqliteVersion: "3.53.1",
|
|
26
|
-
},
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
assert.match(rendered.main, /SQLite_Runtime/);
|
|
30
|
-
assert.match(rendered.main, /3\.53\.1/);
|
|
31
|
-
assert.match(rendered.main, /sqlite-hub --port:PORT/);
|
|
32
|
-
assert.match(rendered.main, /Version_Check/);
|
|
33
|
-
assert.match(rendered.main, /data-action="check-app-version"/);
|
|
34
|
-
assert.match(rendered.main, /Check Updates/);
|
|
35
|
-
assert.match(rendered.main, /No version check has been run yet\./);
|
|
36
|
-
assert.match(rendered.main, /data-section="information"/);
|
|
37
|
-
assert.match(rendered.main, /data-section="api-tokens"/);
|
|
38
|
-
assert.doesNotMatch(rendered.main, /data-form="create-api-token"/);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test("settings view shows available app updates", async () => {
|
|
42
|
-
const { renderSettingsView } = await loadSettingsViewModule();
|
|
43
|
-
const rendered = renderSettingsView({
|
|
44
|
-
settings: {
|
|
45
|
-
loading: false,
|
|
46
|
-
error: null,
|
|
47
|
-
appVersion: "1.0.1",
|
|
48
|
-
sqliteVersion: "3.53.1",
|
|
49
|
-
versionCheckLoading: false,
|
|
50
|
-
versionCheckError: null,
|
|
51
|
-
versionCheck: {
|
|
52
|
-
currentVersion: "1.0.1",
|
|
53
|
-
latestVersion: "1.1.0",
|
|
54
|
-
updateAvailable: true,
|
|
55
|
-
releaseUrl: "https://www.npmjs.com/package/sqlite-hub/v/1.1.0",
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
assert.match(rendered.main, /New Version Available/);
|
|
61
|
-
assert.match(rendered.main, /Current v1\.0\.1 · Latest v1\.1\.0/);
|
|
62
|
-
assert.match(rendered.main, /Open Release/);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test("settings view scopes API token controls to the active database", async () => {
|
|
66
|
-
const { renderSettingsView } = await loadSettingsViewModule();
|
|
67
|
-
const rendered = renderSettingsView({
|
|
68
|
-
settings: {
|
|
69
|
-
loading: false,
|
|
70
|
-
error: null,
|
|
71
|
-
appVersion: "1.2.3",
|
|
72
|
-
sqliteVersion: "3.53.1",
|
|
73
|
-
section: "api-tokens",
|
|
74
|
-
tokenDatabase: { id: "db-one", label: "Database One" },
|
|
75
|
-
apiTokens: [
|
|
76
|
-
{
|
|
77
|
-
id: "token-one",
|
|
78
|
-
name: "Automation",
|
|
79
|
-
tokenPrefix: "shub_example",
|
|
80
|
-
createdAt: "2026-06-15T10:00:00.000Z",
|
|
81
|
-
},
|
|
82
|
-
],
|
|
83
|
-
createdApiToken: null,
|
|
84
|
-
tokenSaving: false,
|
|
85
|
-
},
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
assert.match(rendered.main, /API Tokens/);
|
|
89
|
-
assert.match(rendered.main, /Security \/\/ Database API access/);
|
|
90
|
-
assert.match(rendered.main, /Database One/);
|
|
91
|
-
assert.match(rendered.main, /Database ID/);
|
|
92
|
-
assert.match(rendered.main, /data-database-id/);
|
|
93
|
-
assert.match(rendered.main, /data-action="copy-database-id"/);
|
|
94
|
-
assert.match(rendered.main, /Copy ID/);
|
|
95
|
-
assert.match(rendered.main, /sm:grid-cols-\[minmax\(0,1fr\)_9rem\]/);
|
|
96
|
-
assert.equal((rendered.main.match(/w-full self-center justify-center/g) ?? []).length, 2);
|
|
97
|
-
assert.match(rendered.main, /shub_example\.\.\./);
|
|
98
|
-
assert.match(rendered.main, /data-api-token-name/);
|
|
99
|
-
assert.match(rendered.main, /data-form="create-api-token"/);
|
|
100
|
-
assert.match(rendered.main, /name="name"/);
|
|
101
|
-
assert.match(rendered.main, /type="submit"/);
|
|
102
|
-
assert.match(rendered.main, /data-action="open-delete-api-token-modal"/);
|
|
103
|
-
assert.match(rendered.main, /bg-surface-container-lowest/);
|
|
104
|
-
assert.doesNotMatch(rendered.main, /sqlite-hub --port:PORT/);
|
|
105
|
-
assert.doesNotMatch(rendered.main, /Open Github/);
|
|
106
|
-
assert.doesNotMatch(rendered.main, /token_hash/);
|
|
107
|
-
});
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
const assert = require("node:assert/strict");
|
|
2
|
-
const { readFileSync } = require("node:fs");
|
|
3
|
-
const path = require("node:path");
|
|
4
|
-
const test = require("node:test");
|
|
5
|
-
|
|
6
|
-
let sqlFormatterModulePromise = null;
|
|
7
|
-
|
|
8
|
-
function loadSqlFormatterModule() {
|
|
9
|
-
if (!sqlFormatterModulePromise) {
|
|
10
|
-
const source = readFileSync(
|
|
11
|
-
path.resolve(__dirname, "../frontend/js/utils/sqlFormatter.js"),
|
|
12
|
-
"utf8"
|
|
13
|
-
);
|
|
14
|
-
const url = `data:text/javascript;base64,${Buffer.from(source).toString("base64")}`;
|
|
15
|
-
|
|
16
|
-
sqlFormatterModulePromise = import(url);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
return sqlFormatterModulePromise;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
test("SQL formatter uppercases keywords and formats common clauses", async () => {
|
|
23
|
-
const { formatSqlQuery } = await loadSqlFormatterModule();
|
|
24
|
-
|
|
25
|
-
assert.equal(
|
|
26
|
-
formatSqlQuery("select id,name from users where status='active' and age>=18 order by name desc;"),
|
|
27
|
-
[
|
|
28
|
-
"SELECT id,",
|
|
29
|
-
" name",
|
|
30
|
-
"",
|
|
31
|
-
"FROM users",
|
|
32
|
-
"",
|
|
33
|
-
"WHERE status = 'active'",
|
|
34
|
-
" AND age >= 18",
|
|
35
|
-
"",
|
|
36
|
-
"ORDER BY name DESC;",
|
|
37
|
-
].join("\n")
|
|
38
|
-
);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test("SQL formatter keeps string literals and comments untouched", async () => {
|
|
42
|
-
const { formatSqlQuery } = await loadSqlFormatterModule();
|
|
43
|
-
|
|
44
|
-
assert.equal(
|
|
45
|
-
formatSqlQuery("-- select from comment\nselect 'from where' as note, name from users"),
|
|
46
|
-
[
|
|
47
|
-
"-- select from comment",
|
|
48
|
-
"SELECT 'from where' AS note,",
|
|
49
|
-
" name",
|
|
50
|
-
"",
|
|
51
|
-
"FROM users",
|
|
52
|
-
].join("\n")
|
|
53
|
-
);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test("SQL formatter formats joins on separate readable lines", async () => {
|
|
57
|
-
const { formatSqlQuery } = await loadSqlFormatterModule();
|
|
58
|
-
|
|
59
|
-
assert.equal(
|
|
60
|
-
formatSqlQuery("select * from users left join posts on posts.user_id=users.id where users.id=1"),
|
|
61
|
-
[
|
|
62
|
-
"SELECT *",
|
|
63
|
-
"",
|
|
64
|
-
"FROM users",
|
|
65
|
-
"",
|
|
66
|
-
"LEFT JOIN posts",
|
|
67
|
-
" ON posts.user_id = users.id",
|
|
68
|
-
"",
|
|
69
|
-
"WHERE users.id = 1",
|
|
70
|
-
].join("\n")
|
|
71
|
-
);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test("SQL formatter does not break function arguments as select columns", async () => {
|
|
75
|
-
const { formatSqlQuery } = await loadSqlFormatterModule();
|
|
76
|
-
|
|
77
|
-
assert.equal(
|
|
78
|
-
formatSqlQuery("select coalesce(first_name,last_name) as display_name, id from users"),
|
|
79
|
-
[
|
|
80
|
-
"SELECT coalesce(first_name, last_name) AS display_name,",
|
|
81
|
-
" id",
|
|
82
|
-
"",
|
|
83
|
-
"FROM users",
|
|
84
|
-
].join("\n")
|
|
85
|
-
);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test("SQL formatter indents AS parenthesis blocks", async () => {
|
|
89
|
-
const { formatSqlQuery } = await loadSqlFormatterModule();
|
|
90
|
-
|
|
91
|
-
assert.equal(
|
|
92
|
-
formatSqlQuery("with recent as(select id,name from users where active=1) select * from recent"),
|
|
93
|
-
[
|
|
94
|
-
"WITH recent AS (",
|
|
95
|
-
" SELECT id,",
|
|
96
|
-
" name",
|
|
97
|
-
"",
|
|
98
|
-
" FROM users",
|
|
99
|
-
"",
|
|
100
|
-
" WHERE active = 1",
|
|
101
|
-
")",
|
|
102
|
-
"",
|
|
103
|
-
"SELECT *",
|
|
104
|
-
"",
|
|
105
|
-
"FROM recent",
|
|
106
|
-
].join("\n")
|
|
107
|
-
);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
test("SQL formatter does not treat non-parenthesized AS keywords as blocks", async () => {
|
|
111
|
-
const { formatSqlQuery } = await loadSqlFormatterModule();
|
|
112
|
-
|
|
113
|
-
assert.equal(
|
|
114
|
-
formatSqlQuery("select cast(value as integer) as value_int from metrics"),
|
|
115
|
-
[
|
|
116
|
-
"SELECT CAST(value AS INTEGER) AS value_int",
|
|
117
|
-
"",
|
|
118
|
-
"FROM metrics",
|
|
119
|
-
].join("\n")
|
|
120
|
-
);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
test("SQL formatter expands CASE expressions into readable blocks", async () => {
|
|
124
|
-
const { formatSqlQuery } = await loadSqlFormatterModule();
|
|
125
|
-
|
|
126
|
-
assert.equal(
|
|
127
|
-
formatSqlQuery(
|
|
128
|
-
"select upload_date,case when upload_date is null then null else substr(cast(upload_date as text),1,4)||'-'||substr(cast(upload_date as text),5,2)||'-'||substr(cast(upload_date as text),7,2) end as upload_date_readable from white_house_live_streams"
|
|
129
|
-
),
|
|
130
|
-
[
|
|
131
|
-
"SELECT upload_date,",
|
|
132
|
-
" CASE",
|
|
133
|
-
" WHEN upload_date IS NULL THEN NULL",
|
|
134
|
-
" ELSE",
|
|
135
|
-
" substr(CAST(upload_date AS TEXT), 1, 4) || '-' ||",
|
|
136
|
-
" substr(CAST(upload_date AS TEXT), 5, 2) || '-' ||",
|
|
137
|
-
" substr(CAST(upload_date AS TEXT), 7, 2)",
|
|
138
|
-
" END AS upload_date_readable",
|
|
139
|
-
"",
|
|
140
|
-
"FROM white_house_live_streams",
|
|
141
|
-
].join("\n")
|
|
142
|
-
);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
test("SQL formatter separates window functions and join blocks", async () => {
|
|
146
|
-
const { formatSqlQuery } = await loadSqlFormatterModule();
|
|
147
|
-
|
|
148
|
-
assert.equal(
|
|
149
|
-
formatSqlQuery(
|
|
150
|
-
"with winners as(select w.window_label,row_number() over(partition by w.window_label order by w.delta_return desc) as winner_rank from windowed w join mention_impact mi on mi.id=w.mention_impact_id order by w.window_label,winner_rank) select * from winners"
|
|
151
|
-
),
|
|
152
|
-
[
|
|
153
|
-
"WITH winners AS (",
|
|
154
|
-
" SELECT w.window_label,",
|
|
155
|
-
" ROW_NUMBER() OVER (",
|
|
156
|
-
" PARTITION BY w.window_label",
|
|
157
|
-
" ORDER BY w.delta_return DESC",
|
|
158
|
-
" ) AS winner_rank",
|
|
159
|
-
"",
|
|
160
|
-
" FROM windowed w",
|
|
161
|
-
"",
|
|
162
|
-
" JOIN mention_impact mi",
|
|
163
|
-
" ON mi.id = w.mention_impact_id",
|
|
164
|
-
"",
|
|
165
|
-
" ORDER BY w.window_label, winner_rank",
|
|
166
|
-
")",
|
|
167
|
-
"",
|
|
168
|
-
"SELECT *",
|
|
169
|
-
"",
|
|
170
|
-
"FROM winners",
|
|
171
|
-
].join("\n")
|
|
172
|
-
);
|
|
173
|
-
});
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
const assert = require("node:assert/strict");
|
|
2
|
-
const { readFileSync } = require("node:fs");
|
|
3
|
-
const path = require("node:path");
|
|
4
|
-
const test = require("node:test");
|
|
5
|
-
|
|
6
|
-
let formatModulePromise = null;
|
|
7
|
-
|
|
8
|
-
function loadFormatModule() {
|
|
9
|
-
if (!formatModulePromise) {
|
|
10
|
-
const source = readFileSync(
|
|
11
|
-
path.resolve(__dirname, "../frontend/js/utils/format.js"),
|
|
12
|
-
"utf8"
|
|
13
|
-
);
|
|
14
|
-
const url = `data:text/javascript;base64,${Buffer.from(source).toString("base64")}`;
|
|
15
|
-
|
|
16
|
-
formatModulePromise = import(url);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
return formatModulePromise;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
test("SQL highlighting marks CASE and null-check keywords", async () => {
|
|
23
|
-
const { highlightSql } = await loadFormatModule();
|
|
24
|
-
const highlighted = highlightSql("CASE WHEN upload_date IS NULL THEN NULL ELSE 1 END");
|
|
25
|
-
|
|
26
|
-
for (const keyword of ["CASE", "WHEN", "IS", "NULL", "THEN", "ELSE", "END"]) {
|
|
27
|
-
assert.match(highlighted, new RegExp(`<span class="sql-keyword">${keyword}</span>`));
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test("SQL highlighting marks window function keywords", async () => {
|
|
32
|
-
const { highlightSql } = await loadFormatModule();
|
|
33
|
-
const highlighted = highlightSql("ROW_NUMBER() OVER (PARTITION BY window_label ORDER BY delta_return DESC)");
|
|
34
|
-
|
|
35
|
-
for (const keyword of ["ROW_NUMBER", "OVER", "PARTITION", "ORDER", "BY", "DESC"]) {
|
|
36
|
-
assert.match(highlighted, new RegExp(`<span class="sql-keyword">${keyword}</span>`));
|
|
37
|
-
}
|
|
38
|
-
});
|
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
const Database = require("better-sqlite3");
|
|
2
|
-
const assert = require("node:assert/strict");
|
|
3
|
-
const test = require("node:test");
|
|
4
|
-
const { DataBrowserService } = require("../server/services/sqlite/dataBrowserService");
|
|
5
|
-
const { quoteIdentifier } = require("../server/utils/identifier");
|
|
6
|
-
|
|
7
|
-
test("data browser mutations preserve quoted dynamic identifiers", () => {
|
|
8
|
-
const db = new Database(":memory:");
|
|
9
|
-
const tableName = 'items" archived';
|
|
10
|
-
const valueColumn = 'display"name';
|
|
11
|
-
const noteColumn = 'review"note';
|
|
12
|
-
|
|
13
|
-
try {
|
|
14
|
-
db.exec(
|
|
15
|
-
[
|
|
16
|
-
"CREATE TABLE",
|
|
17
|
-
quoteIdentifier(tableName),
|
|
18
|
-
"(",
|
|
19
|
-
quoteIdentifier(valueColumn),
|
|
20
|
-
"TEXT, status INTEGER,",
|
|
21
|
-
quoteIdentifier(noteColumn),
|
|
22
|
-
"TEXT",
|
|
23
|
-
")",
|
|
24
|
-
].join(" ")
|
|
25
|
-
);
|
|
26
|
-
db.prepare(
|
|
27
|
-
[
|
|
28
|
-
"INSERT INTO",
|
|
29
|
-
quoteIdentifier(tableName),
|
|
30
|
-
"(" + quoteIdentifier(valueColumn) + ", status, " + quoteIdentifier(noteColumn) + ")",
|
|
31
|
-
"VALUES (?, ?, ?)",
|
|
32
|
-
].join(" ")
|
|
33
|
-
).run("before", 0, null);
|
|
34
|
-
|
|
35
|
-
const service = new DataBrowserService({
|
|
36
|
-
connectionManager: {
|
|
37
|
-
assertWritable() {},
|
|
38
|
-
getActiveDatabase: () => db,
|
|
39
|
-
},
|
|
40
|
-
});
|
|
41
|
-
const tableData = service.getTableData(tableName, { limit: 10, offset: 0 });
|
|
42
|
-
const filteredTableData = service.getTableData(tableName, {
|
|
43
|
-
limit: 10,
|
|
44
|
-
offset: 0,
|
|
45
|
-
filterColumn: valueColumn,
|
|
46
|
-
filterOperator: "=",
|
|
47
|
-
filterValue: "EFO",
|
|
48
|
-
});
|
|
49
|
-
const negativeFilteredTableData = service.getTableData(tableName, {
|
|
50
|
-
limit: 10,
|
|
51
|
-
offset: 0,
|
|
52
|
-
filterColumn: valueColumn,
|
|
53
|
-
filterOperator: "!=",
|
|
54
|
-
filterValue: "EFO",
|
|
55
|
-
});
|
|
56
|
-
const exactFilteredTableData = service.getTableData(tableName, {
|
|
57
|
-
limit: 10,
|
|
58
|
-
offset: 0,
|
|
59
|
-
filterColumn: valueColumn,
|
|
60
|
-
filterOperator: "equals",
|
|
61
|
-
filterValue: "BEFORE",
|
|
62
|
-
});
|
|
63
|
-
const exactSubstringMissTableData = service.getTableData(tableName, {
|
|
64
|
-
limit: 10,
|
|
65
|
-
offset: 0,
|
|
66
|
-
filterColumn: valueColumn,
|
|
67
|
-
filterOperator: "equals",
|
|
68
|
-
filterValue: "EFO",
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
assert.equal(filteredTableData.rowCount, 1);
|
|
72
|
-
assert.equal(filteredTableData.rows[0][valueColumn], "before");
|
|
73
|
-
assert.deepEqual(filteredTableData.filter, {
|
|
74
|
-
column: valueColumn,
|
|
75
|
-
operator: "=",
|
|
76
|
-
value: "EFO",
|
|
77
|
-
matchMode: "contains",
|
|
78
|
-
});
|
|
79
|
-
assert.equal(negativeFilteredTableData.rowCount, 0);
|
|
80
|
-
assert.equal(exactFilteredTableData.rowCount, 1);
|
|
81
|
-
assert.equal(exactFilteredTableData.rows[0][valueColumn], "before");
|
|
82
|
-
assert.deepEqual(exactFilteredTableData.filter, {
|
|
83
|
-
column: valueColumn,
|
|
84
|
-
operator: "equals",
|
|
85
|
-
value: "BEFORE",
|
|
86
|
-
matchMode: "equals",
|
|
87
|
-
});
|
|
88
|
-
assert.equal(exactSubstringMissTableData.rowCount, 0);
|
|
89
|
-
assert.throws(
|
|
90
|
-
() =>
|
|
91
|
-
service.getTableData(tableName, {
|
|
92
|
-
limit: 10,
|
|
93
|
-
offset: 0,
|
|
94
|
-
filterColumn: valueColumn,
|
|
95
|
-
filterOperator: "<>",
|
|
96
|
-
filterValue: "before",
|
|
97
|
-
}),
|
|
98
|
-
/filterOperator/
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
const identity = tableData.rows[0].__identity;
|
|
102
|
-
const preview = service.previewTableRowUpdate(tableName, {
|
|
103
|
-
identity,
|
|
104
|
-
values: {
|
|
105
|
-
[valueColumn]: "after",
|
|
106
|
-
status: 0,
|
|
107
|
-
[noteColumn]: "",
|
|
108
|
-
},
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
assert.equal(
|
|
112
|
-
preview.sql,
|
|
113
|
-
[
|
|
114
|
-
"UPDATE",
|
|
115
|
-
quoteIdentifier(tableName),
|
|
116
|
-
"SET",
|
|
117
|
-
quoteIdentifier(valueColumn) + " = ?,",
|
|
118
|
-
quoteIdentifier(noteColumn) + " = ?",
|
|
119
|
-
"WHERE",
|
|
120
|
-
"rowid IS ?",
|
|
121
|
-
].join(" ")
|
|
122
|
-
);
|
|
123
|
-
assert.deepEqual(preview.changes, [
|
|
124
|
-
{
|
|
125
|
-
column: valueColumn,
|
|
126
|
-
oldValue: "before",
|
|
127
|
-
newValue: "after",
|
|
128
|
-
},
|
|
129
|
-
{
|
|
130
|
-
column: noteColumn,
|
|
131
|
-
oldValue: "NULL",
|
|
132
|
-
newValue: "",
|
|
133
|
-
},
|
|
134
|
-
]);
|
|
135
|
-
|
|
136
|
-
const updated = service.updateTableRow(tableName, {
|
|
137
|
-
identity,
|
|
138
|
-
values: {
|
|
139
|
-
[valueColumn]: "after",
|
|
140
|
-
[noteColumn]: "",
|
|
141
|
-
},
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
assert.equal(updated.row[valueColumn], "after");
|
|
145
|
-
assert.equal(updated.row[noteColumn], "");
|
|
146
|
-
const persistedRow = db.prepare(
|
|
147
|
-
[
|
|
148
|
-
"SELECT",
|
|
149
|
-
quoteIdentifier(valueColumn) + ",",
|
|
150
|
-
quoteIdentifier(noteColumn),
|
|
151
|
-
"FROM",
|
|
152
|
-
quoteIdentifier(tableName),
|
|
153
|
-
].join(" ")
|
|
154
|
-
).get();
|
|
155
|
-
|
|
156
|
-
assert.equal(persistedRow[valueColumn], "after");
|
|
157
|
-
assert.equal(persistedRow[noteColumn], "");
|
|
158
|
-
|
|
159
|
-
const deleted = service.deleteTableRow(tableName, {
|
|
160
|
-
identity: updated.row.__identity,
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
assert.equal(deleted.affectedRowCount, 1);
|
|
164
|
-
assert.equal(
|
|
165
|
-
db.prepare(["SELECT COUNT(*) AS count FROM", quoteIdentifier(tableName)].join(" ")).get().count,
|
|
166
|
-
0
|
|
167
|
-
);
|
|
168
|
-
} finally {
|
|
169
|
-
db.close();
|
|
170
|
-
}
|
|
171
|
-
});
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
const Database = require("better-sqlite3");
|
|
2
|
-
const assert = require("node:assert/strict");
|
|
3
|
-
const test = require("node:test");
|
|
4
|
-
const {
|
|
5
|
-
DEFAULT_RESULT_ROW_LIMIT,
|
|
6
|
-
SqlExecutor,
|
|
7
|
-
} = require("../server/services/sqlite/sqlExecutor");
|
|
8
|
-
|
|
9
|
-
function createExecutor(db) {
|
|
10
|
-
return new SqlExecutor({
|
|
11
|
-
connectionManager: {
|
|
12
|
-
getActiveDatabase: () => db,
|
|
13
|
-
getActiveConnection: () => ({ id: "result-limit-test" }),
|
|
14
|
-
},
|
|
15
|
-
appStateStore: {
|
|
16
|
-
recordQueryExecution: () => 1,
|
|
17
|
-
},
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
test("interactive SQL results stop at the configured row limit", () => {
|
|
22
|
-
const db = new Database(":memory:");
|
|
23
|
-
|
|
24
|
-
try {
|
|
25
|
-
db.exec(`
|
|
26
|
-
CREATE TABLE items (id INTEGER PRIMARY KEY, label TEXT);
|
|
27
|
-
INSERT INTO items (label) VALUES ('one'), ('two'), ('three'), ('four'), ('five');
|
|
28
|
-
`);
|
|
29
|
-
|
|
30
|
-
const result = createExecutor(db).execute("SELECT id, label FROM items ORDER BY id", {
|
|
31
|
-
maxRows: 3,
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
assert.equal(DEFAULT_RESULT_ROW_LIMIT, 5000);
|
|
35
|
-
assert.equal(result.rows.length, 3);
|
|
36
|
-
assert.deepEqual(result.rows.map((row) => row.id), [1, 2, 3]);
|
|
37
|
-
assert.equal(result.truncated, true);
|
|
38
|
-
assert.equal(result.rowLimit, 3);
|
|
39
|
-
assert.equal(result.statements[0].truncated, true);
|
|
40
|
-
assert.equal(result.statements[0].rowCount, 3);
|
|
41
|
-
} finally {
|
|
42
|
-
db.close();
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test("unlimited SQL execution remains available for export services", () => {
|
|
47
|
-
const db = new Database(":memory:");
|
|
48
|
-
|
|
49
|
-
try {
|
|
50
|
-
db.exec(`
|
|
51
|
-
CREATE TABLE items (id INTEGER PRIMARY KEY);
|
|
52
|
-
INSERT INTO items VALUES (1), (2), (3), (4), (5);
|
|
53
|
-
`);
|
|
54
|
-
|
|
55
|
-
const result = createExecutor(db).execute("SELECT id FROM items ORDER BY id", {
|
|
56
|
-
maxRows: null,
|
|
57
|
-
persistHistory: false,
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
assert.equal(result.rows.length, 5);
|
|
61
|
-
assert.equal(result.truncated, false);
|
|
62
|
-
assert.equal(result.rowLimit, null);
|
|
63
|
-
} finally {
|
|
64
|
-
db.close();
|
|
65
|
-
}
|
|
66
|
-
});
|