sqlite-hub 1.2.0 → 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.
Files changed (152) hide show
  1. package/README.md +106 -73
  2. package/bin/sqlite-hub.js +176 -2
  3. package/docs/API.md +122 -0
  4. package/docs/CLI.md +227 -0
  5. package/docs/DESIGN_GUIDELINES.md +45 -0
  6. package/docs/changelog.md +141 -0
  7. package/docs/shortkeys.md +27 -0
  8. package/docs/todo.md +16 -0
  9. package/examples/api/generate-types.js +52 -0
  10. package/frontend/assets/mockups/backups_1_1920.webp +0 -0
  11. package/frontend/assets/mockups/backups_2_create_backup_modal_1920.webp +0 -0
  12. package/frontend/assets/mockups/backups_3_edit_backup_modal_1920.webp +0 -0
  13. package/frontend/assets/mockups/backups_4_restore_backup_modal_1920.webp +0 -0
  14. package/frontend/assets/mockups/backups_5_delete_backup_modal_1920.webp +0 -0
  15. package/frontend/assets/mockups/charts_1_1920.webp +0 -0
  16. package/frontend/assets/mockups/charts_2_query_detail_1920.webp +0 -0
  17. package/frontend/assets/mockups/charts_3_create_query_chart_modal_1920.webp +0 -0
  18. package/frontend/assets/mockups/charts_4_edit_query_chart_modal_1920.webp +0 -0
  19. package/frontend/assets/mockups/charts_5_delete_query_chart_modal_1920.webp +0 -0
  20. package/frontend/assets/mockups/charts_6_copy_column_modal_1920.webp +0 -0
  21. package/frontend/assets/mockups/connections_1_1920.webp +0 -0
  22. package/frontend/assets/mockups/connections_2_create_connection_modal_1920.webp +0 -0
  23. package/frontend/assets/mockups/connections_3_open_connection_modal_1920.webp +0 -0
  24. package/frontend/assets/mockups/data_1_1920.webp +0 -0
  25. package/frontend/assets/mockups/data_2_roweditor_1920.webp +0 -0
  26. package/frontend/assets/mockups/data_3_data_export_modal_1920.webp +0 -0
  27. package/frontend/assets/mockups/documents_1_1920.webp +0 -0
  28. package/frontend/assets/mockups/documents_2_document_insert_table_modal_1920.webp +0 -0
  29. package/frontend/assets/mockups/documents_3_document_insert_note_modal_1920.webp +0 -0
  30. package/frontend/assets/mockups/media_tagging_queue_1_1920.webp +0 -0
  31. package/frontend/assets/mockups/media_tagging_setup_1_1920.webp +0 -0
  32. package/frontend/assets/mockups/media_tagging_setup_2_create_media_tagging_tag_table_modal_1920.webp +0 -0
  33. package/frontend/assets/mockups/media_tagging_setup_3_create_media_tagging_mapping_table_modal_1920.webp +0 -0
  34. package/frontend/assets/mockups/overview_1_1920.webp +0 -0
  35. package/frontend/assets/mockups/settings_1_1920.webp +0 -0
  36. package/frontend/assets/mockups/sql_editor_1_1920.webp +0 -0
  37. package/frontend/assets/mockups/sql_editor_2_query_detail_1920.webp +0 -0
  38. package/frontend/assets/mockups/sql_editor_3_query_export_modal_1920.webp +0 -0
  39. package/frontend/assets/mockups/structure_1_1920.webp +0 -0
  40. package/frontend/assets/mockups/structure_2_generate_types_modal_1920.webp +0 -0
  41. package/frontend/assets/mockups/structure_3_generate_types_modal_1920.webp +0 -0
  42. package/frontend/assets/mockups/table_designer_1_1920.webp +0 -0
  43. package/frontend/assets/mockups/table_designer_2_table_designer_constraints_modal_1920.webp +0 -0
  44. package/frontend/js/api.js +18 -0
  45. package/frontend/js/app.js +118 -8
  46. package/frontend/js/components/connectionCard.js +1 -1
  47. package/frontend/js/components/emptyState.js +4 -4
  48. package/frontend/js/components/generateTypesDropdown.js +33 -0
  49. package/frontend/js/components/metricCard.js +1 -1
  50. package/frontend/js/components/modal.js +211 -18
  51. package/frontend/js/components/pageHeader.js +1 -1
  52. package/frontend/js/components/queryHistoryDetail.js +3 -3
  53. package/frontend/js/components/queryHistoryHeader.js +1 -1
  54. package/frontend/js/components/queryHistoryPanel.js +1 -1
  55. package/frontend/js/components/rowEditorPanel.js +13 -3
  56. package/frontend/js/router.js +28 -6
  57. package/frontend/js/store.js +422 -8
  58. package/frontend/js/views/backups.js +529 -37
  59. package/frontend/js/views/charts.js +9 -9
  60. package/frontend/js/views/connections.js +2 -2
  61. package/frontend/js/views/data.js +1 -1
  62. package/frontend/js/views/documents.js +3 -3
  63. package/frontend/js/views/editor.js +6 -6
  64. package/frontend/js/views/mediaTagging.js +7 -5
  65. package/frontend/js/views/overview.js +3 -3
  66. package/frontend/js/views/settings.js +3 -3
  67. package/frontend/js/views/structure.js +6 -1
  68. package/frontend/styles/base.css +1 -40
  69. package/frontend/styles/components.css +109 -232
  70. package/frontend/styles/structure-graph.css +32 -68
  71. package/frontend/styles/tailwind.css +8 -4
  72. package/frontend/styles/tailwind.generated.css +1 -1
  73. package/frontend/styles/tokens.css +28 -21
  74. package/frontend/styles/views.css +94 -261
  75. package/package.json +18 -2
  76. package/server/routes/backups.js +18 -2
  77. package/server/routes/externalApi.js +36 -0
  78. package/server/routes/structure.js +22 -0
  79. package/server/services/databaseCommandService.js +14 -0
  80. package/server/services/sqlite/backupDiff.js +914 -0
  81. package/server/services/sqlite/backupService.js +77 -1
  82. package/server/services/sqlite/structureService.js +7 -0
  83. package/server/services/typeGenerationService.js +663 -0
  84. package/tailwind.config.cjs +0 -1
  85. package/.github/funding.yml +0 -2
  86. package/.github/workflows/ci.yml +0 -36
  87. package/database.sqlite +0 -0
  88. package/frontend/assets/mockups/charts_1_bars_1200.webp +0 -0
  89. package/frontend/assets/mockups/charts_2_pie_1200.webp +0 -0
  90. package/frontend/assets/mockups/charts_3_scatter_plot_1200.webp +0 -0
  91. package/frontend/assets/mockups/connections_1200.webp +0 -0
  92. package/frontend/assets/mockups/data_1_1200.webp +0 -0
  93. package/frontend/assets/mockups/data_2_row_editor_1200.webp +0 -0
  94. package/frontend/assets/mockups/documents_1200.webp +0 -0
  95. package/frontend/assets/mockups/media_tagging_1_setup_1200.webp +0 -0
  96. package/frontend/assets/mockups/media_tagging_2_tagging_queue_1200.webp +0 -0
  97. package/frontend/assets/mockups/media_tagging_3_media_viewer_1200.webp +0 -0
  98. package/frontend/assets/mockups/overview_1200.webp +0 -0
  99. package/frontend/assets/mockups/settings_1200.webp +0 -0
  100. package/frontend/assets/mockups/sql_editor_1_1200.webp +0 -0
  101. package/frontend/assets/mockups/sql_editor_2_query_details_1200.webp +0 -0
  102. package/frontend/assets/mockups/sql_editor_3_export_column_1200.webp +0 -0
  103. package/frontend/assets/mockups/sql_editor_4_export_column_as_markdown_1200.webp +0 -0
  104. package/frontend/assets/mockups/sql_editor_5_export_query_result_1200.webp +0 -0
  105. package/frontend/assets/mockups/structure_1_1200.webp +0 -0
  106. package/frontend/assets/mockups/structure_2_inspector_1200.webp +0 -0
  107. package/frontend/assets/mockups/table_designer_1_1200.webp +0 -0
  108. package/frontend/assets/mockups/table_designer_2_checks_1200.webp +0 -0
  109. package/tests/api-token-auth.test.js +0 -279
  110. package/tests/backup-manager.test.js +0 -140
  111. package/tests/backups-view.test.js +0 -70
  112. package/tests/charts-height-preset-storage.test.js +0 -60
  113. package/tests/charts-route-state.test.js +0 -144
  114. package/tests/check-constraint-options.test.js +0 -90
  115. package/tests/cli-args.test.js +0 -113
  116. package/tests/cli-service-delegation.test.js +0 -140
  117. package/tests/connection-removal.test.js +0 -52
  118. package/tests/connections-file-dialog-route.test.js +0 -89
  119. package/tests/copy-column-modal.test.js +0 -131
  120. package/tests/database-command-service.test.js +0 -174
  121. package/tests/database-documents.test.js +0 -85
  122. package/tests/documents-view.test.js +0 -132
  123. package/tests/dropdown-button.test.js +0 -101
  124. package/tests/email-preview.test.js +0 -89
  125. package/tests/export-blob.test.js +0 -99
  126. package/tests/export-filenames.test.js +0 -38
  127. package/tests/file-path-preview.test.js +0 -165
  128. package/tests/form-controls.test.js +0 -34
  129. package/tests/json-preview.test.js +0 -49
  130. package/tests/local-request-security.test.js +0 -85
  131. package/tests/markdown-documents.test.js +0 -103
  132. package/tests/native-file-dialog.test.js +0 -105
  133. package/tests/query-editor.test.js +0 -28
  134. package/tests/query-history-detail.test.js +0 -37
  135. package/tests/query-history-header.test.js +0 -30
  136. package/tests/query-results-truncation.test.js +0 -20
  137. package/tests/risky-sql.test.js +0 -30
  138. package/tests/row-editor-json.test.js +0 -82
  139. package/tests/row-editor-null-values.test.js +0 -184
  140. package/tests/row-editor-timestamp-preview.test.js +0 -192
  141. package/tests/security-paths.test.js +0 -84
  142. package/tests/settings-api-tokens-route.test.js +0 -97
  143. package/tests/settings-metadata.test.js +0 -114
  144. package/tests/settings-view.test.js +0 -108
  145. package/tests/sql-formatter.test.js +0 -173
  146. package/tests/sql-highlight.test.js +0 -38
  147. package/tests/sql-identifier-safety.test.js +0 -171
  148. package/tests/sql-result-limit.test.js +0 -66
  149. package/tests/structure-view.test.js +0 -60
  150. package/tests/table-designer-v2-unique-constraints.test.js +0 -78
  151. package/tests/table-scroll-state.test.js +0 -70
  152. 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
- });