sqlite-hub 1.1.1 → 1.1.2

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 (48) hide show
  1. package/README.md +17 -196
  2. package/bin/sqlite-hub.js +7 -2
  3. package/frontend/js/api.js +60 -0
  4. package/frontend/js/app.js +110 -0
  5. package/frontend/js/components/dropdownButton.js +92 -0
  6. package/frontend/js/components/modal.js +991 -876
  7. package/frontend/js/components/queryEditor.js +24 -15
  8. package/frontend/js/components/queryHistoryDetail.js +20 -1
  9. package/frontend/js/components/queryHistoryHeader.js +3 -2
  10. package/frontend/js/components/rowEditorPanel.js +1 -0
  11. package/frontend/js/components/sidebar.js +1 -0
  12. package/frontend/js/router.js +2 -0
  13. package/frontend/js/store.js +383 -37
  14. package/frontend/js/utils/riskySql.js +165 -0
  15. package/frontend/js/views/backups.js +176 -0
  16. package/frontend/js/views/charts.js +1 -1
  17. package/frontend/js/views/documents.js +184 -154
  18. package/frontend/js/views/structure.js +26 -24
  19. package/frontend/styles/components.css +128 -0
  20. package/frontend/styles/tailwind.generated.css +1 -1
  21. package/frontend/styles/views.css +33 -21
  22. package/package.json +2 -1
  23. package/server/routes/backups.js +120 -0
  24. package/server/routes/connections.js +26 -3
  25. package/server/routes/externalApi.js +6 -2
  26. package/server/server.js +3 -1
  27. package/server/services/databaseCommandService.js +4 -3
  28. package/server/services/sqlite/backupService.js +497 -66
  29. package/server/services/sqlite/connectionManager.js +2 -2
  30. package/server/services/sqlite/importService.js +25 -0
  31. package/server/services/sqlite/sqlExecutor.js +2 -0
  32. package/server/services/storage/appStateStore.js +379 -88
  33. package/tests/api-token-auth.test.js +45 -2
  34. package/tests/backup-manager.test.js +140 -0
  35. package/tests/backups-view.test.js +64 -0
  36. package/tests/charts-height-preset-storage.test.js +60 -0
  37. package/tests/charts-route-state.test.js +144 -0
  38. package/tests/cli-service-delegation.test.js +97 -0
  39. package/tests/connection-removal.test.js +52 -0
  40. package/tests/database-command-service.test.js +37 -2
  41. package/tests/documents-view.test.js +132 -0
  42. package/tests/dropdown-button.test.js +75 -0
  43. package/tests/query-editor.test.js +28 -0
  44. package/tests/query-history-detail.test.js +37 -0
  45. package/tests/query-history-header.test.js +30 -0
  46. package/tests/risky-sql.test.js +30 -0
  47. package/tests/row-editor-null-values.test.js +24 -0
  48. package/tests/structure-view.test.js +56 -0
@@ -0,0 +1,140 @@
1
+ const assert = require("node:assert/strict");
2
+ const fs = require("node:fs");
3
+ const os = require("node:os");
4
+ const path = require("node:path");
5
+ const test = require("node:test");
6
+ const Database = require("better-sqlite3");
7
+
8
+ const { BackupService } = require("../server/services/sqlite/backupService");
9
+ const { ConnectionManager } = require("../server/services/sqlite/connectionManager");
10
+ const { AppStateStore } = require("../server/services/storage/appStateStore");
11
+
12
+ function createFixture(t) {
13
+ const directory = fs.mkdtempSync(path.join(os.tmpdir(), "sqlite-hub-backups-"));
14
+ const databasePath = path.join(directory, "source.sqlite");
15
+ const db = new Database(databasePath);
16
+
17
+ db.exec(`
18
+ CREATE TABLE companies (id INTEGER PRIMARY KEY, name TEXT NOT NULL);
19
+ INSERT INTO companies (name) VALUES ('Acme'), ('Globex');
20
+ `);
21
+ db.close();
22
+
23
+ const store = new AppStateStore(path.join(directory, "state.db"));
24
+ const connectionManager = new ConnectionManager({ appStateStore: store });
25
+ const connection = connectionManager.openConnection({
26
+ filePath: databasePath,
27
+ label: "Source",
28
+ id: "conn_source",
29
+ makeActive: true,
30
+ });
31
+ const backupService = new BackupService({
32
+ appStateStore: store,
33
+ connectionManager,
34
+ backupRootDirectory: path.join(directory, "SQLite Hub", "backups"),
35
+ });
36
+
37
+ t.after(() => {
38
+ connectionManager.closeCurrent();
39
+ store.db.close();
40
+ fs.rmSync(directory, { recursive: true, force: true });
41
+ });
42
+
43
+ return {
44
+ backupService,
45
+ connection,
46
+ connectionManager,
47
+ directory,
48
+ store,
49
+ };
50
+ }
51
+
52
+ test("backups table is created with recent_connections foreign key and checks", (t) => {
53
+ const { store } = createFixture(t);
54
+ const tables = new Set(
55
+ store.db.prepare("SELECT name FROM sqlite_schema WHERE type = 'table'").all().map(row => row.name)
56
+ );
57
+ const foreignKeys = store.db.prepare("PRAGMA foreign_key_list(backups)").all();
58
+
59
+ assert.equal(tables.has("backups"), true);
60
+ assert.equal(foreignKeys[0].table, "recent_connections");
61
+ assert.equal(foreignKeys[0].from, "connectionId");
62
+ assert.equal(foreignKeys[0].on_delete, "SET NULL");
63
+
64
+ assert.throws(
65
+ () =>
66
+ store.createBackupRecord({
67
+ id: "invalid",
68
+ connectionId: "conn_source",
69
+ name: "Invalid",
70
+ path: path.join(os.tmpdir(), "invalid.sqlite"),
71
+ status: "unknown",
72
+ type: "manual",
73
+ sourcePath: path.join(os.tmpdir(), "source.sqlite"),
74
+ createdAt: new Date().toISOString(),
75
+ }),
76
+ /CHECK constraint failed/
77
+ );
78
+ });
79
+
80
+ test("manual backup creates file, metadata, manifest, and survives connection removal", async (t) => {
81
+ const { backupService, connection, store } = createFixture(t);
82
+
83
+ const backup = await backupService.createActiveBackup({
84
+ name: "Before migration",
85
+ notes: "Schema update",
86
+ });
87
+ const manifestPath = path.join(
88
+ backupService.backupRootDirectory,
89
+ connection.id,
90
+ "manifest.json"
91
+ );
92
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
93
+ const stored = store.getBackup(backup.id);
94
+
95
+ assert.equal(backup.status, "verified");
96
+ assert.equal(backup.connectionId, connection.id);
97
+ assert.equal(backup.notes, "Schema update");
98
+ assert.equal(fs.existsSync(backup.path), true);
99
+ assert.match(path.relative(backupService.backupRootDirectory, backup.path), /^conn_source\//);
100
+ assert.equal(stored.checksumSha256.length, 64);
101
+ assert.equal(stored.tableCount, 1);
102
+ assert.equal(stored.rowCount, 2);
103
+ assert.equal(manifest.databaseId, connection.id);
104
+ assert.equal(manifest.backups[0].id, backup.id);
105
+ assert.equal(manifest.backups[0].status, "verified");
106
+
107
+ store.removeRecentConnection(connection.id);
108
+ assert.equal(store.getBackup(backup.id).connectionId, null);
109
+ });
110
+
111
+ test("backup notes can be updated after creation and manifest stays in sync", async (t) => {
112
+ const { backupService, connection, store } = createFixture(t);
113
+
114
+ const backup = await backupService.createActiveBackup({
115
+ name: "Before migration",
116
+ notes: "Initial note",
117
+ });
118
+ const updated = backupService.updateBackupNotes(backup.id, "Reviewed after restore test");
119
+ const manifestPath = path.join(
120
+ backupService.backupRootDirectory,
121
+ connection.id,
122
+ "manifest.json"
123
+ );
124
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
125
+
126
+ assert.equal(updated.notes, "Reviewed after restore test");
127
+ assert.equal(store.getBackup(backup.id).notes, "Reviewed after restore test");
128
+ assert.equal(manifest.backups[0].notes, "Reviewed after restore test");
129
+ });
130
+
131
+ test("delete backup removes file, record, and manifest entry", async (t) => {
132
+ const { backupService, store } = createFixture(t);
133
+ const backup = await backupService.createActiveBackup({ name: "Manual backup" });
134
+ const backupPath = backup.path;
135
+
136
+ backupService.deleteBackup(backup.id);
137
+
138
+ assert.equal(fs.existsSync(backupPath), false);
139
+ assert.equal(store.getBackup(backup.id), null);
140
+ });
@@ -0,0 +1,64 @@
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
+ async function loadModule() {
7
+ return import(pathToFileURL(path.resolve(__dirname, "../frontend/js/views/backups.js")).href);
8
+ }
9
+
10
+ function createState(overrides = {}) {
11
+ return {
12
+ connections: {
13
+ active: { id: "db-one", label: "Customers", readOnly: false },
14
+ },
15
+ backups: {
16
+ items: [],
17
+ loading: false,
18
+ operationLoading: false,
19
+ error: null,
20
+ },
21
+ ...overrides,
22
+ };
23
+ }
24
+
25
+ test("backup manager renders empty state and create action", async () => {
26
+ const { renderBackupsView } = await loadModule();
27
+ const rendered = renderBackupsView(createState()).main;
28
+
29
+ assert.match(rendered, /No backups yet/);
30
+ assert.match(rendered, /data-action="open-create-backup-modal"/);
31
+ });
32
+
33
+ test("backup manager renders backup rows with status and actions", async () => {
34
+ const { renderBackupsView } = await loadModule();
35
+ const rendered = renderBackupsView(
36
+ createState({
37
+ backups: {
38
+ items: [
39
+ {
40
+ id: "backup-one",
41
+ name: "Before migration",
42
+ notes: "Schema update",
43
+ sizeBytes: 1024,
44
+ status: "verified",
45
+ fileExists: true,
46
+ fileName: "backup.sqlite",
47
+ path: "/tmp/backup.sqlite",
48
+ createdAt: "2026-06-21T11:42:18.000Z",
49
+ },
50
+ ],
51
+ loading: false,
52
+ operationLoading: false,
53
+ error: null,
54
+ },
55
+ })
56
+ ).main;
57
+
58
+ assert.match(rendered, /Before migration/);
59
+ assert.match(rendered, /Verified/);
60
+ assert.match(rendered, /data-action="open-edit-backup-notes-modal"/);
61
+ assert.match(rendered, /data-action="open-restore-backup-modal"/);
62
+ assert.match(rendered, /data-action="download-backup"/);
63
+ assert.match(rendered, /data-action="open-delete-backup-modal"/);
64
+ });
@@ -0,0 +1,60 @@
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
+ const CHARTS_HEIGHT_STORAGE_KEY = "sqlite_hub_charts_height_preset";
7
+
8
+ function createMockStorage(initialValues = {}) {
9
+ const entries = new Map(Object.entries(initialValues));
10
+
11
+ return {
12
+ getItem(key) {
13
+ return entries.has(key) ? entries.get(key) : null;
14
+ },
15
+ setItem(key, value) {
16
+ entries.set(key, String(value));
17
+ },
18
+ };
19
+ }
20
+
21
+ async function importStoreWithMockStorage(storage) {
22
+ const originalDescriptor = Object.getOwnPropertyDescriptor(globalThis, "localStorage");
23
+
24
+ Object.defineProperty(globalThis, "localStorage", {
25
+ configurable: true,
26
+ value: storage,
27
+ });
28
+
29
+ const moduleUrl = pathToFileURL(path.resolve(__dirname, "../frontend/js/store.js")).href;
30
+ const store = await import(`${moduleUrl}?charts-height=${Date.now()}`);
31
+
32
+ return {
33
+ restore() {
34
+ if (originalDescriptor) {
35
+ Object.defineProperty(globalThis, "localStorage", originalDescriptor);
36
+ } else {
37
+ delete globalThis.localStorage;
38
+ }
39
+ },
40
+ store,
41
+ };
42
+ }
43
+
44
+ test("charts height preset is read from and written to localStorage", async () => {
45
+ const storage = createMockStorage({
46
+ [CHARTS_HEIGHT_STORAGE_KEY]: "large",
47
+ });
48
+ const { restore, store } = await importStoreWithMockStorage(storage);
49
+
50
+ try {
51
+ assert.equal(store.getState().charts.chartHeightPreset, "large");
52
+
53
+ store.setChartsHeightPreset("small");
54
+
55
+ assert.equal(store.getState().charts.chartHeightPreset, "small");
56
+ assert.equal(storage.getItem(CHARTS_HEIGHT_STORAGE_KEY), "small");
57
+ } finally {
58
+ restore();
59
+ }
60
+ });
@@ -0,0 +1,144 @@
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
+ function jsonResponse(payload) {
7
+ return new Response(JSON.stringify({ success: true, ...payload }), {
8
+ headers: { "content-type": "application/json" },
9
+ status: 200,
10
+ });
11
+ }
12
+
13
+ function createChartsFetchMock() {
14
+ const activeConnection = {
15
+ id: "db-one",
16
+ label: "Database One",
17
+ readOnly: false,
18
+ };
19
+ const chartableQuery = {
20
+ id: 10,
21
+ displayTitle: "Revenue Chart",
22
+ previewSql: "select revenue from metrics",
23
+ rawSql: "select revenue from metrics",
24
+ queryType: "select",
25
+ isSaved: false,
26
+ isDestructive: false,
27
+ chartTypes: ["bar"],
28
+ };
29
+
30
+ return async function fetchMock(input, options = {}) {
31
+ const url = typeof input === "string" ? input : input.url;
32
+ const method = String(options.method ?? "GET").toUpperCase();
33
+
34
+ if (method === "GET" && url === "/api/connections/recent") {
35
+ return jsonResponse({ data: [activeConnection] });
36
+ }
37
+
38
+ if (method === "GET" && url === "/api/connections/active") {
39
+ return jsonResponse({ data: activeConnection });
40
+ }
41
+
42
+ if (method === "GET" && url === "/api/settings") {
43
+ return jsonResponse({
44
+ data: {},
45
+ metadata: {
46
+ activeDatabase: activeConnection,
47
+ apiTokens: [],
48
+ appVersion: "1.1.2",
49
+ sqliteVersion: "3.50.0",
50
+ },
51
+ });
52
+ }
53
+
54
+ if (method === "GET" && url.startsWith("/api/sql/history")) {
55
+ return jsonResponse({ data: { items: [], total: 0, hasMore: false } });
56
+ }
57
+
58
+ if (method === "GET" && url === "/api/db/overview") {
59
+ return jsonResponse({ data: { tables: [] } });
60
+ }
61
+
62
+ if (method === "GET" && url === "/api/charts/query-history") {
63
+ return jsonResponse({ data: [chartableQuery] });
64
+ }
65
+
66
+ if (method === "GET" && url === "/api/charts/query-history/10") {
67
+ return jsonResponse({
68
+ data: {
69
+ item: chartableQuery,
70
+ charts: [],
71
+ },
72
+ });
73
+ }
74
+
75
+ if (method === "POST" && url === "/api/charts/query-history/10/execute") {
76
+ return jsonResponse({
77
+ data: {
78
+ columns: ["revenue"],
79
+ rows: [{ revenue: 100 }],
80
+ sql: chartableQuery.rawSql,
81
+ },
82
+ });
83
+ }
84
+
85
+ return new Response(
86
+ JSON.stringify({
87
+ success: false,
88
+ error: { code: "UNEXPECTED_REQUEST", message: `${method} ${url}` },
89
+ }),
90
+ {
91
+ headers: { "content-type": "application/json" },
92
+ status: 500,
93
+ },
94
+ );
95
+ };
96
+ }
97
+
98
+ async function importStoreWithFetch(fetchMock) {
99
+ const originalFetch = globalThis.fetch;
100
+
101
+ globalThis.fetch = fetchMock;
102
+
103
+ const moduleUrl = pathToFileURL(path.resolve(__dirname, "../frontend/js/store.js")).href;
104
+ const store = await import(`${moduleUrl}?charts-route-state=${Date.now()}`);
105
+
106
+ return {
107
+ restore() {
108
+ globalThis.fetch = originalFetch;
109
+ },
110
+ store,
111
+ };
112
+ }
113
+
114
+ test("charts route preserves the selected chart when returning from another menu", async () => {
115
+ const { restore, store } = await importStoreWithFetch(createChartsFetchMock());
116
+
117
+ try {
118
+ await store.initializeApp();
119
+ await store.setRoute({
120
+ name: "charts",
121
+ params: { historyId: "10" },
122
+ path: "/charts/10",
123
+ });
124
+
125
+ assert.equal(store.getState().charts.selectedHistoryId, 10);
126
+ assert.equal(store.getState().charts.detail?.item?.id, 10);
127
+
128
+ await store.setRoute({
129
+ name: "overview",
130
+ params: {},
131
+ path: "/overview",
132
+ });
133
+ await store.setRoute({
134
+ name: "charts",
135
+ params: { historyId: null },
136
+ path: "/charts",
137
+ });
138
+
139
+ assert.equal(store.getState().charts.selectedHistoryId, 10);
140
+ assert.equal(store.getState().charts.detail?.item?.id, 10);
141
+ } finally {
142
+ restore();
143
+ }
144
+ });
@@ -41,3 +41,100 @@ test("CLI delegates database operations to the shared command service", async ()
41
41
  ]);
42
42
  assert.match(output.join("\n"), /companies/);
43
43
  });
44
+
45
+ test("CLI marks raw query executions as cli", async () => {
46
+ const calls = [];
47
+ const connection = {
48
+ id: "db-one",
49
+ label: "Database One",
50
+ };
51
+ const databaseService = {
52
+ getDatabase(reference) {
53
+ calls.push(["getDatabase", reference]);
54
+ return connection;
55
+ },
56
+ listDatabases() {
57
+ calls.push(["listDatabases"]);
58
+ return [connection];
59
+ },
60
+ executeRawQuery(reference, sql, options = {}) {
61
+ calls.push(["executeRawQuery", reference, sql, options]);
62
+ return {
63
+ result: {
64
+ statementCount: 1,
65
+ timingMs: 1,
66
+ statements: [],
67
+ historyId: 7,
68
+ },
69
+ storedQuery: null,
70
+ };
71
+ },
72
+ };
73
+ const output = [];
74
+ const originalLog = console.log;
75
+
76
+ console.log = (...values) => output.push(values.join(" "));
77
+
78
+ try {
79
+ await main(["--database:Database One", "--query:SELECT 1"], { databaseService });
80
+ } finally {
81
+ console.log = originalLog;
82
+ }
83
+
84
+ assert.deepEqual(calls, [
85
+ ["listDatabases"],
86
+ ["getDatabase", "Database One"],
87
+ ["executeRawQuery", "db-one", "SELECT 1", { storeName: null, executedBy: "cli" }],
88
+ ]);
89
+ assert.match(output.join("\n"), /History ID: 7/);
90
+ });
91
+
92
+ test("CLI marks saved query executions as cli", async () => {
93
+ const calls = [];
94
+ const connection = {
95
+ id: "db-one",
96
+ label: "Database One",
97
+ };
98
+ const databaseService = {
99
+ getDatabase(reference) {
100
+ calls.push(["getDatabase", reference]);
101
+ return connection;
102
+ },
103
+ listDatabases() {
104
+ calls.push(["listDatabases"]);
105
+ return [connection];
106
+ },
107
+ executeSavedQuery(reference, queryName, options = {}) {
108
+ calls.push(["executeSavedQuery", reference, queryName, options]);
109
+ return {
110
+ query: {
111
+ title: queryName,
112
+ rawSql: "SELECT 1",
113
+ },
114
+ result: {
115
+ statementCount: 1,
116
+ timingMs: 1,
117
+ statements: [],
118
+ historyId: 8,
119
+ },
120
+ };
121
+ },
122
+ };
123
+ const output = [];
124
+ const originalLog = console.log;
125
+
126
+ console.log = (...values) => output.push(values.join(" "));
127
+
128
+ try {
129
+ await main(["--database:Database One", "--execute:Hype-Reversal"], { databaseService });
130
+ } finally {
131
+ console.log = originalLog;
132
+ }
133
+
134
+ assert.deepEqual(calls, [
135
+ ["listDatabases"],
136
+ ["getDatabase", "Database One"],
137
+ ["executeSavedQuery", "db-one", "Hype-Reversal", { executedBy: "cli" }],
138
+ ]);
139
+ assert.match(output.join("\n"), /Executing: Hype-Reversal/);
140
+ });
@@ -0,0 +1,52 @@
1
+ const assert = require("node:assert/strict");
2
+ const fs = require("node:fs");
3
+ const os = require("node:os");
4
+ const path = require("node:path");
5
+ const test = require("node:test");
6
+
7
+ const { ConnectionManager } = require("../server/services/sqlite/connectionManager");
8
+ const { AppStateStore } = require("../server/services/storage/appStateStore");
9
+
10
+ function createStore(t) {
11
+ const directory = fs.mkdtempSync(path.join(os.tmpdir(), "sqlite-hub-connection-remove-"));
12
+ const store = new AppStateStore(path.join(directory, "state.db"));
13
+
14
+ t.after(() => {
15
+ store.db.close();
16
+ fs.rmSync(directory, { recursive: true, force: true });
17
+ });
18
+
19
+ return { directory, store };
20
+ }
21
+
22
+ test("removing a recent connection does not require legacy sql_history", (t) => {
23
+ const { directory, store } = createStore(t);
24
+ const firstConnection = {
25
+ id: "db-one",
26
+ label: "One",
27
+ path: path.join(directory, "one.db"),
28
+ lastOpenedAt: "2026-06-21T10:00:00.000Z",
29
+ lastModifiedAt: null,
30
+ sizeBytes: 0,
31
+ readOnly: false,
32
+ logoPath: null,
33
+ };
34
+ const secondConnection = {
35
+ ...firstConnection,
36
+ id: "db-two",
37
+ label: "Two",
38
+ path: path.join(directory, "two.db"),
39
+ };
40
+ const manager = new ConnectionManager({ appStateStore: store });
41
+
42
+ store.upsertRecentConnection(firstConnection);
43
+ store.upsertRecentConnection(secondConnection, { makeActive: false });
44
+ store.db.exec("DROP TABLE sql_history");
45
+
46
+ const remaining = manager.removeRecentConnection(firstConnection.id);
47
+
48
+ assert.deepEqual(
49
+ remaining.map((connection) => connection.id),
50
+ ["db-two"]
51
+ );
52
+ });
@@ -59,6 +59,29 @@ function createFixture(t, options = {}) {
59
59
  new Date().toISOString(),
60
60
  new Date().toISOString()
61
61
  );
62
+ store.db
63
+ .prepare(
64
+ `
65
+ INSERT INTO query_history (
66
+ database_key,
67
+ normalized_sql,
68
+ raw_sql,
69
+ query_type,
70
+ tables_detected,
71
+ is_saved,
72
+ first_executed_at,
73
+ last_used_at
74
+ )
75
+ VALUES (?, ?, ?, 'other', '[]', 0, ?, ?)
76
+ `
77
+ )
78
+ .run(
79
+ connection.id,
80
+ "company list",
81
+ "Company List",
82
+ new Date().toISOString(),
83
+ new Date().toISOString()
84
+ );
62
85
  store.createDatabaseDocument(connection.id, {
63
86
  filename: "Readme.md",
64
87
  content: "# Sample\n",
@@ -77,7 +100,7 @@ function createFixture(t, options = {}) {
77
100
  }
78
101
 
79
102
  test("database command service provides shared CLI and API operations", (t) => {
80
- const { connection, service } = createFixture(t);
103
+ const { connection, service, store } = createFixture(t);
81
104
 
82
105
  assert.equal(service.getDatabase("sample").id, connection.id);
83
106
  assert.deepEqual(service.listTables(connection.id), [{ name: "companies", columnCount: 2 }]);
@@ -89,10 +112,18 @@ test("database command service provides shared CLI and API operations", (t) => {
89
112
 
90
113
  const queries = service.listSavedQueries(connection.id);
91
114
  assert.equal(queries.total, 1);
92
- assert.equal(service.getSavedQuery(connection.id, "Company List").notes, "Used by CLI and API");
115
+ const savedQuery = service.getSavedQuery(connection.id, "Company List");
116
+ assert.equal(savedQuery.notes, "Used by CLI and API");
117
+ assert.equal(savedQuery.rawSql, "SELECT id, name FROM companies ORDER BY id");
93
118
 
94
119
  const execution = service.executeSavedQuery(connection.id, "Company List");
95
120
  assert.equal(execution.result.statements[0].rowCount, 2);
121
+ assert.equal(execution.result.historyId, savedQuery.id);
122
+
123
+ const savedRun = store.db
124
+ .prepare("SELECT executed_by, status FROM query_runs WHERE history_id = ? ORDER BY id DESC LIMIT 1")
125
+ .get(savedQuery.id);
126
+ assert.deepEqual(savedRun, { executed_by: "user", status: "success" });
96
127
 
97
128
  const exported = service.exportSavedQuery(connection.id, "Company List", "csv");
98
129
  assert.equal(exported.result.rowCount, 2);
@@ -118,6 +149,9 @@ test("raw query execution writes SQL Editor query history", (t) => {
118
149
  const historyRow = store.db
119
150
  .prepare("SELECT raw_sql, query_type, title, is_saved FROM query_history WHERE id = ?")
120
151
  .get(result.historyId);
152
+ const runRow = store.db
153
+ .prepare("SELECT executed_by FROM query_runs WHERE history_id = ? ORDER BY id DESC LIMIT 1")
154
+ .get(result.historyId);
121
155
 
122
156
  assert.equal(result.affectedRowCount, 1);
123
157
  assert.equal(afterCount, beforeCount + 1);
@@ -125,6 +159,7 @@ test("raw query execution writes SQL Editor query history", (t) => {
125
159
  assert.equal(historyRow.query_type, "insert");
126
160
  assert.equal(historyRow.title, "Add Initech");
127
161
  assert.equal(historyRow.is_saved, 1);
162
+ assert.equal(runRow.executed_by, "user");
128
163
  assert.equal(storedQuery.title, "Add Initech");
129
164
  assert.equal(storedQuery.isSaved, true);
130
165
  });