sqlite-hub 0.12.0 → 0.17.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 (64) hide show
  1. package/README.md +118 -23
  2. package/bin/sqlite-hub.js +1041 -224
  3. package/frontend/index.html +1 -0
  4. package/frontend/js/api.js +32 -2
  5. package/frontend/js/app.js +989 -13
  6. package/frontend/js/components/modal.js +644 -15
  7. package/frontend/js/components/pageHeader.js +14 -16
  8. package/frontend/js/components/queryEditor.js +9 -1
  9. package/frontend/js/components/queryHistoryPanel.js +126 -131
  10. package/frontend/js/components/queryResults.js +79 -17
  11. package/frontend/js/components/rowEditorPanel.js +204 -10
  12. package/frontend/js/components/sidebar.js +102 -18
  13. package/frontend/js/components/tableDesignerEditor.js +69 -11
  14. package/frontend/js/router.js +8 -0
  15. package/frontend/js/store.js +868 -9
  16. package/frontend/js/utils/copyColumnExport.js +117 -0
  17. package/frontend/js/utils/exportFilenames.js +32 -0
  18. package/frontend/js/utils/filePathPreview.js +315 -0
  19. package/frontend/js/utils/format.js +1 -1
  20. package/frontend/js/utils/markdownDocuments.js +248 -0
  21. package/frontend/js/utils/rowEditorJson.js +65 -0
  22. package/frontend/js/utils/sqlFormatter.js +691 -0
  23. package/frontend/js/utils/tableDesigner.js +178 -6
  24. package/frontend/js/utils/textCellStats.js +20 -0
  25. package/frontend/js/utils/timestampPreview.js +264 -0
  26. package/frontend/js/views/charts.js +3 -1
  27. package/frontend/js/views/data.js +34 -2
  28. package/frontend/js/views/documents.js +300 -0
  29. package/frontend/js/views/editor.js +48 -1
  30. package/frontend/js/views/settings.js +39 -6
  31. package/frontend/js/views/structure.js +154 -212
  32. package/frontend/styles/base.css +6 -0
  33. package/frontend/styles/components.css +476 -2
  34. package/frontend/styles/structure-graph.css +0 -3
  35. package/frontend/styles/tailwind.generated.css +1 -1
  36. package/frontend/styles/tokens.css +1 -1
  37. package/frontend/styles/views.css +422 -0
  38. package/package.json +3 -3
  39. package/server/routes/documents.js +163 -0
  40. package/server/routes/settings.js +22 -6
  41. package/server/server.js +6 -0
  42. package/server/services/sqlite/introspection.js +10 -0
  43. package/server/services/sqlite/sqlExecutor.js +29 -0
  44. package/server/services/sqlite/tableDesigner/changeAnalysis.js +25 -1
  45. package/server/services/sqlite/tableDesigner/schemaMapping.js +105 -10
  46. package/server/services/sqlite/tableDesigner/validation.js +60 -2
  47. package/server/services/sqlite/tableDesignerService.js +1 -1
  48. package/server/services/storage/appStateStore.js +313 -0
  49. package/tests/check-constraint-options.test.js +14 -0
  50. package/tests/cli-args.test.js +100 -0
  51. package/tests/copy-column-modal.test.js +83 -0
  52. package/tests/database-documents.test.js +85 -0
  53. package/tests/export-filenames.test.js +34 -0
  54. package/tests/file-path-preview.test.js +165 -0
  55. package/tests/markdown-documents.test.js +79 -0
  56. package/tests/row-editor-json.test.js +82 -0
  57. package/tests/row-editor-timestamp-preview.test.js +192 -0
  58. package/tests/settings-metadata.test.js +16 -0
  59. package/tests/settings-view.test.js +32 -0
  60. package/tests/sql-formatter.test.js +173 -0
  61. package/tests/sql-highlight.test.js +38 -0
  62. package/tests/table-designer-v2-unique-constraints.test.js +78 -0
  63. package/tests/text-cell-stats.test.js +38 -0
  64. package/fill.js +0 -526
@@ -0,0 +1,173 @@
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
+ });
@@ -0,0 +1,38 @@
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
+ });
@@ -0,0 +1,78 @@
1
+ const Database = require("better-sqlite3");
2
+ const assert = require("node:assert/strict");
3
+ const test = require("node:test");
4
+ const { getTableDetail } = require("../server/services/sqlite/introspection");
5
+ const { analyzeTableDesignerChanges } = require("../server/services/sqlite/tableDesigner/changeAnalysis");
6
+ const { buildTableDesignerDraft } = require("../server/services/sqlite/tableDesigner/schemaMapping");
7
+
8
+ test("table designer draft exposes v2 complex unique constraints", () => {
9
+ const db = new Database(":memory:");
10
+
11
+ try {
12
+ db.exec(`
13
+ CREATE TABLE companies (
14
+ company_id INTEGER NOT NULL,
15
+ ticker TEXT NOT NULL,
16
+ deleted_at TEXT,
17
+ name TEXT,
18
+ UNIQUE (company_id, ticker)
19
+ );
20
+ CREATE UNIQUE INDEX uniq_active_company_ticker
21
+ ON companies (ticker)
22
+ WHERE deleted_at IS NULL;
23
+ `);
24
+
25
+ const tableDetail = getTableDetail(db, "companies", { includeRowCount: false });
26
+ const draft = buildTableDesignerDraft(tableDetail);
27
+
28
+ assert.equal(draft.designerVersion, 2);
29
+ assert.equal(draft.uniqueConstraints.length, 2);
30
+
31
+ const multiColumnConstraint = draft.uniqueConstraints.find(
32
+ (constraint) => constraint.columns.length === 2
33
+ );
34
+ assert.ok(multiColumnConstraint);
35
+ assert.equal(multiColumnConstraint.partial, false);
36
+ assert.deepEqual(
37
+ multiColumnConstraint.columns.map((column) => column.name),
38
+ ["company_id", "ticker"]
39
+ );
40
+ assert.equal(multiColumnConstraint.expression, 'UNIQUE ("company_id", "ticker")');
41
+ assert.equal(multiColumnConstraint.originalExpression, multiColumnConstraint.expression);
42
+ assert.equal(multiColumnConstraint.editable, true);
43
+ assert.equal(multiColumnConstraint.preserved, true);
44
+
45
+ const partialConstraint = draft.uniqueConstraints.find((constraint) => constraint.partial);
46
+ assert.ok(partialConstraint);
47
+ assert.equal(partialConstraint.name, "uniq_active_company_ticker");
48
+ assert.match(partialConstraint.sql, /CREATE UNIQUE INDEX uniq_active_company_ticker/i);
49
+ assert.match(partialConstraint.sql, /WHERE deleted_at IS NULL/i);
50
+ assert.equal(partialConstraint.expression, partialConstraint.sql);
51
+
52
+ const changedDraft = {
53
+ ...draft,
54
+ uniqueConstraints: draft.uniqueConstraints.map((constraint) =>
55
+ constraint.id === multiColumnConstraint.id
56
+ ? {
57
+ ...constraint,
58
+ expression: 'UNIQUE ("company_id", "ticker", "name")',
59
+ }
60
+ : constraint
61
+ ),
62
+ };
63
+ const analysis = analyzeTableDesignerChanges({ draft: changedDraft, originalDraft: draft });
64
+ assert.equal(analysis.dirty, true);
65
+ assert.equal(analysis.executable, false);
66
+ assert.match(analysis.warnings[0].title, /Constraint Change Requires Rebuild/);
67
+
68
+ const uniqueWarning = draft.schemaWarnings.find(
69
+ (warning) => warning.code === "COMPLEX_UNIQUE_CONSTRAINTS_PRESENT"
70
+ );
71
+ assert.ok(uniqueWarning);
72
+ assert.equal(uniqueWarning.title, "Table Designer v2 Unique Constraints");
73
+ assert.equal(uniqueWarning.tone, "muted");
74
+ assert.doesNotMatch(uniqueWarning.message, /outside Table Designer v1/i);
75
+ } finally {
76
+ db.close();
77
+ }
78
+ });
@@ -0,0 +1,38 @@
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 textCellStatsModulePromise = null;
7
+
8
+ function loadTextCellStatsModule() {
9
+ if (!textCellStatsModulePromise) {
10
+ const source = readFileSync(
11
+ path.resolve(__dirname, "../frontend/js/utils/textCellStats.js"),
12
+ "utf8"
13
+ );
14
+ const url = `data:text/javascript;base64,${Buffer.from(source).toString("base64")}`;
15
+
16
+ textCellStatsModulePromise = import(url);
17
+ }
18
+
19
+ return textCellStatsModulePromise;
20
+ }
21
+
22
+ test("text cell character count only returns counts for non-empty strings", async () => {
23
+ const { getTextCellCharacterCount } = await loadTextCellStatsModule();
24
+
25
+ assert.equal(getTextCellCharacterCount(""), null);
26
+ assert.equal(getTextCellCharacterCount(null), null);
27
+ assert.equal(getTextCellCharacterCount(123), null);
28
+ assert.equal(getTextCellCharacterCount("hello"), 5);
29
+ assert.equal(getTextCellCharacterCount("a\nb"), 3);
30
+ });
31
+
32
+ test("text cell character count label is readable", async () => {
33
+ const { formatTextCellCharacterCount } = await loadTextCellStatsModule();
34
+
35
+ assert.equal(formatTextCellCharacterCount(1), "1 char");
36
+ assert.equal(formatTextCellCharacterCount(25), "25 chars");
37
+ assert.equal(formatTextCellCharacterCount(1200), "1,200 chars");
38
+ });