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.
Files changed (169) hide show
  1. package/README.md +113 -71
  2. package/bin/sqlite-hub.js +723 -116
  3. package/docs/API.md +152 -0
  4. package/docs/CLI.md +249 -0
  5. package/docs/CLI_API_PARITY.md +61 -0
  6. package/docs/changelog.md +149 -0
  7. package/docs/guidelines/AGENTS.md +32 -0
  8. package/docs/guidelines/DESIGN.md +55 -0
  9. package/docs/shortkeys.md +27 -0
  10. package/docs/todo.md +4 -0
  11. package/examples/api/generate-types.js +52 -0
  12. package/frontend/assets/mockups/backups_1_1920.webp +0 -0
  13. package/frontend/assets/mockups/backups_2_create_backup_modal_1920.webp +0 -0
  14. package/frontend/assets/mockups/backups_3_edit_backup_modal_1920.webp +0 -0
  15. package/frontend/assets/mockups/backups_4_restore_backup_modal_1920.webp +0 -0
  16. package/frontend/assets/mockups/backups_5_delete_backup_modal_1920.webp +0 -0
  17. package/frontend/assets/mockups/charts_1_1920.webp +0 -0
  18. package/frontend/assets/mockups/charts_2_query_detail_1920.webp +0 -0
  19. package/frontend/assets/mockups/charts_3_create_query_chart_modal_1920.webp +0 -0
  20. package/frontend/assets/mockups/charts_4_edit_query_chart_modal_1920.webp +0 -0
  21. package/frontend/assets/mockups/charts_5_delete_query_chart_modal_1920.webp +0 -0
  22. package/frontend/assets/mockups/charts_6_copy_column_modal_1920.webp +0 -0
  23. package/frontend/assets/mockups/connections_1_1920.webp +0 -0
  24. package/frontend/assets/mockups/connections_2_create_connection_modal_1920.webp +0 -0
  25. package/frontend/assets/mockups/connections_3_open_connection_modal_1920.webp +0 -0
  26. package/frontend/assets/mockups/data_1_1920.webp +0 -0
  27. package/frontend/assets/mockups/data_2_roweditor_1920.webp +0 -0
  28. package/frontend/assets/mockups/data_3_data_export_modal_1920.webp +0 -0
  29. package/frontend/assets/mockups/documents_1_1920.webp +0 -0
  30. package/frontend/assets/mockups/documents_2_document_insert_table_modal_1920.webp +0 -0
  31. package/frontend/assets/mockups/documents_3_document_insert_note_modal_1920.webp +0 -0
  32. package/frontend/assets/mockups/media_tagging_queue_1_1920.webp +0 -0
  33. package/frontend/assets/mockups/media_tagging_setup_1_1920.webp +0 -0
  34. package/frontend/assets/mockups/media_tagging_setup_2_create_media_tagging_tag_table_modal_1920.webp +0 -0
  35. package/frontend/assets/mockups/media_tagging_setup_3_create_media_tagging_mapping_table_modal_1920.webp +0 -0
  36. package/frontend/assets/mockups/overview_1_1920.webp +0 -0
  37. package/frontend/assets/mockups/settings_1_1920.webp +0 -0
  38. package/frontend/assets/mockups/sql_editor_1_1920.webp +0 -0
  39. package/frontend/assets/mockups/sql_editor_2_query_detail_1920.webp +0 -0
  40. package/frontend/assets/mockups/sql_editor_3_query_export_modal_1920.webp +0 -0
  41. package/frontend/assets/mockups/structure_1_1920.webp +0 -0
  42. package/frontend/assets/mockups/structure_2_generate_types_modal_1920.webp +0 -0
  43. package/frontend/assets/mockups/structure_3_generate_types_modal_1920.webp +0 -0
  44. package/frontend/assets/mockups/table_designer_1_1920.webp +0 -0
  45. package/frontend/assets/mockups/table_designer_2_table_designer_constraints_modal_1920.webp +0 -0
  46. package/frontend/js/api.js +63 -0
  47. package/frontend/js/app.js +310 -10
  48. package/frontend/js/components/connectionCard.js +4 -2
  49. package/frontend/js/components/emptyState.js +4 -4
  50. package/frontend/js/components/generateTypesDropdown.js +33 -0
  51. package/frontend/js/components/metricCard.js +1 -1
  52. package/frontend/js/components/modal.js +555 -21
  53. package/frontend/js/components/pageHeader.js +1 -1
  54. package/frontend/js/components/queryHistoryDetail.js +3 -3
  55. package/frontend/js/components/queryHistoryHeader.js +1 -1
  56. package/frontend/js/components/queryHistoryPanel.js +1 -1
  57. package/frontend/js/components/rowEditorPanel.js +13 -3
  58. package/frontend/js/components/sidebar.js +41 -7
  59. package/frontend/js/components/topNav.js +2 -14
  60. package/frontend/js/router.js +38 -6
  61. package/frontend/js/store.js +870 -8
  62. package/frontend/js/utils/inputClear.js +36 -0
  63. package/frontend/js/utils/syntheticData.js +240 -0
  64. package/frontend/js/views/backups.js +581 -37
  65. package/frontend/js/views/charts.js +9 -9
  66. package/frontend/js/views/connections.js +2 -2
  67. package/frontend/js/views/data.js +17 -1
  68. package/frontend/js/views/documents.js +3 -3
  69. package/frontend/js/views/editor.js +6 -6
  70. package/frontend/js/views/logs.js +339 -0
  71. package/frontend/js/views/mediaTagging.js +7 -5
  72. package/frontend/js/views/overview.js +3 -3
  73. package/frontend/js/views/settings.js +80 -30
  74. package/frontend/js/views/structure.js +12 -1
  75. package/frontend/js/views/tableAdvisor.js +385 -0
  76. package/frontend/js/views/tableDesigner.js +6 -0
  77. package/frontend/styles/base.css +1 -40
  78. package/frontend/styles/components.css +258 -232
  79. package/frontend/styles/structure-graph.css +32 -68
  80. package/frontend/styles/tailwind.css +8 -4
  81. package/frontend/styles/tailwind.generated.css +1 -1
  82. package/frontend/styles/tokens.css +28 -21
  83. package/frontend/styles/views.css +102 -261
  84. package/package.json +18 -2
  85. package/server/routes/backups.js +18 -2
  86. package/server/routes/data.js +45 -0
  87. package/server/routes/externalApi.js +321 -2
  88. package/server/routes/logs.js +127 -0
  89. package/server/routes/structure.js +22 -0
  90. package/server/server.js +3 -0
  91. package/server/services/databaseCommandService.js +59 -0
  92. package/server/services/sqlite/backupDiff.js +914 -0
  93. package/server/services/sqlite/backupService.js +77 -1
  94. package/server/services/sqlite/dataBrowserService.js +36 -0
  95. package/server/services/sqlite/introspection.js +226 -22
  96. package/server/services/sqlite/structureService.js +7 -0
  97. package/server/services/sqlite/syntheticDataGenerator.js +728 -0
  98. package/server/services/sqlite/tableAdvisor.js +790 -0
  99. package/server/services/storage/appStateStore.js +773 -169
  100. package/server/services/typeGenerationService.js +663 -0
  101. package/tailwind.config.cjs +0 -1
  102. package/.github/funding.yml +0 -2
  103. package/.github/workflows/ci.yml +0 -36
  104. package/database.sqlite +0 -0
  105. package/frontend/assets/mockups/charts_1_bars_1200.webp +0 -0
  106. package/frontend/assets/mockups/charts_2_pie_1200.webp +0 -0
  107. package/frontend/assets/mockups/charts_3_scatter_plot_1200.webp +0 -0
  108. package/frontend/assets/mockups/connections_1200.webp +0 -0
  109. package/frontend/assets/mockups/data_1_1200.webp +0 -0
  110. package/frontend/assets/mockups/data_2_row_editor_1200.webp +0 -0
  111. package/frontend/assets/mockups/documents_1200.webp +0 -0
  112. package/frontend/assets/mockups/media_tagging_1_setup_1200.webp +0 -0
  113. package/frontend/assets/mockups/media_tagging_2_tagging_queue_1200.webp +0 -0
  114. package/frontend/assets/mockups/media_tagging_3_media_viewer_1200.webp +0 -0
  115. package/frontend/assets/mockups/overview_1200.webp +0 -0
  116. package/frontend/assets/mockups/settings_1200.webp +0 -0
  117. package/frontend/assets/mockups/sql_editor_1_1200.webp +0 -0
  118. package/frontend/assets/mockups/sql_editor_2_query_details_1200.webp +0 -0
  119. package/frontend/assets/mockups/sql_editor_3_export_column_1200.webp +0 -0
  120. package/frontend/assets/mockups/sql_editor_4_export_column_as_markdown_1200.webp +0 -0
  121. package/frontend/assets/mockups/sql_editor_5_export_query_result_1200.webp +0 -0
  122. package/frontend/assets/mockups/structure_1_1200.webp +0 -0
  123. package/frontend/assets/mockups/structure_2_inspector_1200.webp +0 -0
  124. package/frontend/assets/mockups/table_designer_1_1200.webp +0 -0
  125. package/frontend/assets/mockups/table_designer_2_checks_1200.webp +0 -0
  126. package/tests/api-token-auth.test.js +0 -279
  127. package/tests/backup-manager.test.js +0 -140
  128. package/tests/backups-view.test.js +0 -70
  129. package/tests/charts-height-preset-storage.test.js +0 -60
  130. package/tests/charts-route-state.test.js +0 -144
  131. package/tests/check-constraint-options.test.js +0 -90
  132. package/tests/cli-args.test.js +0 -113
  133. package/tests/cli-service-delegation.test.js +0 -140
  134. package/tests/connection-removal.test.js +0 -52
  135. package/tests/connections-file-dialog-route.test.js +0 -89
  136. package/tests/copy-column-modal.test.js +0 -131
  137. package/tests/database-command-service.test.js +0 -174
  138. package/tests/database-documents.test.js +0 -85
  139. package/tests/documents-view.test.js +0 -132
  140. package/tests/dropdown-button.test.js +0 -101
  141. package/tests/email-preview.test.js +0 -89
  142. package/tests/export-blob.test.js +0 -99
  143. package/tests/export-filenames.test.js +0 -38
  144. package/tests/file-path-preview.test.js +0 -165
  145. package/tests/form-controls.test.js +0 -34
  146. package/tests/json-preview.test.js +0 -49
  147. package/tests/local-request-security.test.js +0 -85
  148. package/tests/markdown-documents.test.js +0 -103
  149. package/tests/native-file-dialog.test.js +0 -105
  150. package/tests/query-editor.test.js +0 -28
  151. package/tests/query-history-detail.test.js +0 -37
  152. package/tests/query-history-header.test.js +0 -30
  153. package/tests/query-results-truncation.test.js +0 -20
  154. package/tests/risky-sql.test.js +0 -30
  155. package/tests/row-editor-json.test.js +0 -82
  156. package/tests/row-editor-null-values.test.js +0 -184
  157. package/tests/row-editor-timestamp-preview.test.js +0 -192
  158. package/tests/security-paths.test.js +0 -84
  159. package/tests/settings-api-tokens-route.test.js +0 -97
  160. package/tests/settings-metadata.test.js +0 -114
  161. package/tests/settings-view.test.js +0 -108
  162. package/tests/sql-formatter.test.js +0 -173
  163. package/tests/sql-highlight.test.js +0 -38
  164. package/tests/sql-identifier-safety.test.js +0 -171
  165. package/tests/sql-result-limit.test.js +0 -66
  166. package/tests/structure-view.test.js +0 -60
  167. package/tests/table-designer-v2-unique-constraints.test.js +0 -78
  168. package/tests/table-scroll-state.test.js +0 -70
  169. package/tests/text-cell-stats.test.js +0 -38
@@ -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
- });
@@ -1,60 +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 structureViewModulePromise = null;
7
-
8
- function loadStructureViewModule() {
9
- if (!structureViewModulePromise) {
10
- structureViewModulePromise = import(
11
- pathToFileURL(path.resolve(__dirname, "../frontend/js/views/structure.js")).href
12
- );
13
- }
14
-
15
- return structureViewModulePromise;
16
- }
17
-
18
- function buildStructureState() {
19
- return {
20
- structure: {
21
- data: {
22
- grouped: {
23
- tables: [{ name: "companies", type: "table" }],
24
- views: [],
25
- indexes: [],
26
- triggers: [],
27
- },
28
- graph: {
29
- relationshipCount: 0,
30
- tables: [{ name: "companies", type: "table", columns: [], foreignKeys: [] }],
31
- },
32
- },
33
- detail: null,
34
- detailLoading: false,
35
- error: null,
36
- loading: false,
37
- selectedName: "companies",
38
- tableSearchQuery: "",
39
- tablesVisible: true,
40
- },
41
- };
42
- }
43
-
44
- test("structure toolbar groups graph format actions in a dropdown", async () => {
45
- const { renderStructureView } = await loadStructureViewModule();
46
- const { main } = renderStructureView(buildStructureState());
47
-
48
- assert.match(main, /data-dropdown-button/);
49
- assert.match(main, /Open companies/);
50
- assert.match(main, /data-to="\/data\/companies"/);
51
- assert.match(main, /data-to="\/table-designer\/companies"/);
52
- assert.match(main, /data-action="open-table-in-sql-editor"/);
53
- assert.match(main, /Format graph/);
54
- assert.match(main, /Fit Graph/);
55
- assert.match(main, /data-structure-graph-action="fit"/);
56
- assert.match(main, /Recalculate Layout/);
57
- assert.match(main, /data-structure-graph-action="relayout"/);
58
- assert.match(main, /Clear Selection/);
59
- assert.match(main, /data-structure-graph-action="clear"/);
60
- });
@@ -1,78 +0,0 @@
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
- });
@@ -1,70 +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 tableScrollStateModulePromise = null;
7
-
8
- function loadTableScrollStateModule() {
9
- if (!tableScrollStateModulePromise) {
10
- const source = readFileSync(
11
- path.resolve(__dirname, "../frontend/js/utils/tableScrollState.js"),
12
- "utf8"
13
- );
14
- const url = `data:text/javascript;base64,${Buffer.from(source).toString("base64")}`;
15
-
16
- tableScrollStateModulePromise = import(url);
17
- }
18
-
19
- return tableScrollStateModulePromise;
20
- }
21
-
22
- test("table scroll state restores horizontal position for the matching grid", async () => {
23
- const { captureTableHorizontalScrollState, restoreTableHorizontalScrollState } =
24
- await loadTableScrollStateModule();
25
- const key = 'data:events "archive"';
26
- const previousGrid = { dataset: { tableScrollKey: key }, scrollLeft: 640 };
27
- const nextGrid = { dataset: { tableScrollKey: key }, scrollLeft: 0 };
28
- const snapshot = captureTableHorizontalScrollState({
29
- routeName: "data",
30
- scrollNodes: [previousGrid],
31
- });
32
-
33
- assert.equal(
34
- restoreTableHorizontalScrollState({
35
- snapshot,
36
- routeName: "data",
37
- scrollNodes: [nextGrid],
38
- }),
39
- true
40
- );
41
- assert.equal(nextGrid.scrollLeft, 640);
42
- });
43
-
44
- test("table scroll state is not restored across routes or different grids", async () => {
45
- const { captureTableHorizontalScrollState, restoreTableHorizontalScrollState } =
46
- await loadTableScrollStateModule();
47
- const snapshot = captureTableHorizontalScrollState({
48
- routeName: "editorResults",
49
- scrollNodes: [{ dataset: { tableScrollKey: "editor:12" }, scrollLeft: 320 }],
50
- });
51
- const nextGrid = { dataset: { tableScrollKey: "editor:13" }, scrollLeft: 0 };
52
-
53
- assert.equal(
54
- restoreTableHorizontalScrollState({
55
- snapshot,
56
- routeName: "data",
57
- scrollNodes: [nextGrid],
58
- }),
59
- false
60
- );
61
- assert.equal(
62
- restoreTableHorizontalScrollState({
63
- snapshot,
64
- routeName: "editorResults",
65
- scrollNodes: [nextGrid],
66
- }),
67
- false
68
- );
69
- assert.equal(nextGrid.scrollLeft, 0);
70
- });
@@ -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 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
- });