sqlite-hub 0.11.1 → 0.16.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 (52) hide show
  1. package/README.md +52 -13
  2. package/database.sqlite +0 -0
  3. package/frontend/js/api.js +64 -12
  4. package/frontend/js/app.js +723 -37
  5. package/frontend/js/components/emptyState.js +42 -46
  6. package/frontend/js/components/modal.js +526 -2
  7. package/frontend/js/components/queryEditor.js +12 -3
  8. package/frontend/js/components/queryResults.js +79 -17
  9. package/frontend/js/components/rowEditorPanel.js +345 -12
  10. package/frontend/js/components/sidebar.js +106 -23
  11. package/frontend/js/components/tableDesignerEditor.js +69 -11
  12. package/frontend/js/store.js +437 -26
  13. package/frontend/js/utils/copyColumnExport.js +117 -0
  14. package/frontend/js/utils/exportFilenames.js +32 -0
  15. package/frontend/js/utils/filePathPreview.js +315 -0
  16. package/frontend/js/utils/format.js +1 -1
  17. package/frontend/js/utils/rowEditorJson.js +65 -0
  18. package/frontend/js/utils/sqlFormatter.js +691 -0
  19. package/frontend/js/utils/tableDesigner.js +178 -6
  20. package/frontend/js/utils/textCellStats.js +20 -0
  21. package/frontend/js/utils/timestampPreview.js +264 -0
  22. package/frontend/js/views/charts.js +3 -1
  23. package/frontend/js/views/data.js +39 -4
  24. package/frontend/js/views/editor.js +50 -1
  25. package/frontend/js/views/settings.js +1 -4
  26. package/frontend/js/views/structure.js +154 -212
  27. package/frontend/styles/base.css +6 -0
  28. package/frontend/styles/components.css +463 -2
  29. package/frontend/styles/structure-graph.css +0 -3
  30. package/frontend/styles/tailwind.generated.css +1 -1
  31. package/frontend/styles/tokens.css +95 -95
  32. package/package.json +2 -3
  33. package/server/routes/export.js +97 -12
  34. package/server/services/sqlite/dataBrowserService.js +2 -68
  35. package/server/services/sqlite/exportService.js +74 -15
  36. package/server/services/sqlite/introspection.js +209 -1
  37. package/server/services/sqlite/sqlExecutor.js +31 -0
  38. package/server/services/sqlite/tableDesigner/changeAnalysis.js +25 -1
  39. package/server/services/sqlite/tableDesigner/schemaMapping.js +105 -10
  40. package/server/services/sqlite/tableDesigner/validation.js +60 -2
  41. package/server/services/sqlite/tableDesignerService.js +1 -1
  42. package/server/services/sqlite/tableFilter.js +75 -0
  43. package/server/utils/csv.js +30 -4
  44. package/tests/check-constraint-options.test.js +90 -0
  45. package/tests/export-filenames.test.js +34 -0
  46. package/tests/file-path-preview.test.js +165 -0
  47. package/tests/row-editor-json.test.js +82 -0
  48. package/tests/row-editor-timestamp-preview.test.js +192 -0
  49. package/tests/sql-formatter.test.js +173 -0
  50. package/tests/sql-highlight.test.js +38 -0
  51. package/tests/table-designer-v2-unique-constraints.test.js +78 -0
  52. package/tests/text-cell-stats.test.js +38 -0
@@ -74,6 +74,95 @@ function buildSimpleForeignKeyMap(foreignKeys = []) {
74
74
  return map;
75
75
  }
76
76
 
77
+ function buildUniqueConstraintExpression(index = {}) {
78
+ const columns = (index.columns ?? [])
79
+ .map((column) => column?.name)
80
+ .filter(Boolean);
81
+
82
+ if (!columns.length) {
83
+ return "UNIQUE constraint";
84
+ }
85
+
86
+ return `UNIQUE (${columns.map((columnName) => quoteIdentifier(columnName)).join(", ")})`;
87
+ }
88
+
89
+ function mapComplexUniqueConstraint(index = {}) {
90
+ const sql = String(index.sql ?? "").trim();
91
+ const columns = (index.columns ?? [])
92
+ .map((column) => ({
93
+ name: column.name ?? "",
94
+ descending: Boolean(column.descending),
95
+ collation: column.collation ?? "",
96
+ }))
97
+ .filter((column) => column.name);
98
+
99
+ return {
100
+ id: `unique:${index.name}`,
101
+ name: index.name ?? "",
102
+ originalName: index.name ?? "",
103
+ columns,
104
+ partial: Boolean(index.partial),
105
+ origin: index.origin ?? "",
106
+ sql,
107
+ originalSql: sql,
108
+ expression: sql || buildUniqueConstraintExpression(index),
109
+ originalExpression: sql || buildUniqueConstraintExpression(index),
110
+ editable: true,
111
+ preserved: true,
112
+ };
113
+ }
114
+
115
+ function buildComplexUniqueConstraints(indexes = []) {
116
+ return indexes
117
+ .filter((index) => index?.unique && (index.partial || (index.columns?.length ?? 0) !== 1))
118
+ .map(mapComplexUniqueConstraint);
119
+ }
120
+
121
+ function expressionMentionsColumn(expression = "", columnName = "") {
122
+ const normalizedExpression = String(expression ?? "").toLowerCase();
123
+ const normalizedColumn = String(columnName ?? "").toLowerCase();
124
+
125
+ if (!normalizedColumn) {
126
+ return false;
127
+ }
128
+
129
+ return (
130
+ new RegExp(`(^|[^a-z0-9_$])${normalizedColumn.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}([^a-z0-9_$]|$)`).test(
131
+ normalizedExpression
132
+ ) ||
133
+ normalizedExpression.includes(`"${normalizedColumn.replaceAll('"', '""')}"`) ||
134
+ normalizedExpression.includes(`\`${normalizedColumn.replaceAll("`", "``")}\``) ||
135
+ normalizedExpression.includes(`[${normalizedColumn}]`)
136
+ );
137
+ }
138
+
139
+ function buildCheckConstraints(tableDetail) {
140
+ const visibleColumns = (tableDetail.columns ?? []).filter(
141
+ (column) => column.visible !== false && !column.generated
142
+ );
143
+
144
+ return (tableDetail.checkConstraints ?? []).map((constraint, index) => {
145
+ const expression = String(constraint.expression ?? "").trim();
146
+ const columns = visibleColumns
147
+ .filter((column) => expressionMentionsColumn(expression, column.name))
148
+ .map((column) => ({
149
+ name: column.name,
150
+ allowedValues: Array.isArray(column.allowedValues) ? column.allowedValues : [],
151
+ }));
152
+
153
+ return {
154
+ id: `check:${constraint.id ?? index}`,
155
+ name: `CHECK ${index + 1}`,
156
+ originalName: `CHECK ${index + 1}`,
157
+ columns,
158
+ expression: expression ? `CHECK (${expression})` : "CHECK constraint",
159
+ originalExpression: expression ? `CHECK (${expression})` : "CHECK constraint",
160
+ editable: true,
161
+ preserved: true,
162
+ };
163
+ });
164
+ }
165
+
77
166
  function buildSchemaWarnings(tableDetail) {
78
167
  const warnings = [];
79
168
  const generatedColumns = (tableDetail.columns ?? []).filter((column) => column.generated);
@@ -83,9 +172,7 @@ function buildSchemaWarnings(tableDetail) {
83
172
  const complexForeignKeys = (tableDetail.foreignKeys ?? []).filter(
84
173
  (foreignKey) => (foreignKey.mappings?.length ?? 0) !== 1
85
174
  );
86
- const complexUniqueIndexes = (tableDetail.indexes ?? []).filter(
87
- (index) => index.unique && (index.partial || (index.columns?.length ?? 0) !== 1)
88
- );
175
+ const complexUniqueConstraints = buildComplexUniqueConstraints(tableDetail.indexes);
89
176
 
90
177
  if (generatedColumns.length) {
91
178
  warnings.push(
@@ -93,7 +180,7 @@ function buildSchemaWarnings(tableDetail) {
93
180
  code: "GENERATED_COLUMNS_PRESENT",
94
181
  title: "Generated Columns Detected",
95
182
  message:
96
- "Generated or hidden columns are not editable in Table Designer v1. Safe operations like table rename or adding simple columns still work.",
183
+ "Generated or hidden columns are inspectable but not editable in Table Designer v2. Safe operations like table rename or adding simple columns still work.",
97
184
  })
98
185
  );
99
186
  }
@@ -104,7 +191,7 @@ function buildSchemaWarnings(tableDetail) {
104
191
  code: "COMPOSITE_PRIMARY_KEY_PRESENT",
105
192
  title: "Composite Primary Key Detected",
106
193
  message:
107
- "This table uses more than one primary key column. Table Designer v1 preserves it, but changing primary key structure requires a manual table rebuild.",
194
+ "This table uses more than one primary key column. Table Designer v2 preserves it, but changing primary key structure requires a manual table rebuild.",
108
195
  })
109
196
  );
110
197
  }
@@ -115,18 +202,19 @@ function buildSchemaWarnings(tableDetail) {
115
202
  code: "COMPLEX_FOREIGN_KEYS_PRESENT",
116
203
  title: "Complex Foreign Keys Detected",
117
204
  message:
118
- "Composite or multi-mapping foreign keys cannot be edited directly in Table Designer v1. They are preserved until a rebuild is done manually.",
205
+ "Composite or multi-mapping foreign keys cannot be edited directly in Table Designer v2. They are preserved until a rebuild is done manually.",
119
206
  })
120
207
  );
121
208
  }
122
209
 
123
- if (complexUniqueIndexes.length) {
210
+ if (complexUniqueConstraints.length) {
124
211
  warnings.push(
125
212
  createDesignerWarning({
126
213
  code: "COMPLEX_UNIQUE_CONSTRAINTS_PRESENT",
127
- title: "Complex Unique Constraints Detected",
214
+ title: "Table Designer v2 Unique Constraints",
128
215
  message:
129
- "Multi-column or partial UNIQUE constraints are outside Table Designer v1. They remain untouched unless you rebuild the table manually.",
216
+ "Multi-column and partial UNIQUE constraints are detected, shown in the v2 constraints panel, and preserved by SQLite-safe saves. Editing them still requires manual SQL review.",
217
+ tone: "muted",
130
218
  })
131
219
  );
132
220
  }
@@ -149,7 +237,7 @@ function buildSchemaWarnings(tableDetail) {
149
237
  code: "WITHOUT_ROWID_PRESENT",
150
238
  title: "WITHOUT ROWID Table",
151
239
  message:
152
- "WITHOUT ROWID tables can be inspected here, but rebuild-style changes are intentionally blocked in v1.",
240
+ "WITHOUT ROWID tables can be inspected here, but rebuild-style changes are intentionally blocked in v2.",
153
241
  tone: "muted",
154
242
  })
155
243
  );
@@ -189,6 +277,8 @@ function mapTableColumnToDraft(column, { uniqueColumns, foreignKeyMap }) {
189
277
  function buildTableDesignerDraft(tableDetail) {
190
278
  const uniqueColumns = buildSingleColumnUniqueSet(tableDetail.indexes);
191
279
  const foreignKeyMap = buildSimpleForeignKeyMap(tableDetail.foreignKeys);
280
+ const uniqueConstraints = buildComplexUniqueConstraints(tableDetail.indexes);
281
+ const checkConstraints = buildCheckConstraints(tableDetail);
192
282
  const columns = (tableDetail.columns ?? [])
193
283
  .filter((column) => column.visible !== false && !column.generated)
194
284
  .map((column) => mapTableColumnToDraft(column, { uniqueColumns, foreignKeyMap }));
@@ -199,6 +289,9 @@ function buildTableDesignerDraft(tableDetail) {
199
289
  originalTableName: tableDetail.name,
200
290
  tableName: tableDetail.name,
201
291
  columns,
292
+ uniqueConstraints,
293
+ checkConstraints,
294
+ designerVersion: 2,
202
295
  dirty: false,
203
296
  schemaWarnings,
204
297
  warnings: [...schemaWarnings],
@@ -227,6 +320,8 @@ function listDesignerTables(db) {
227
320
  module.exports = {
228
321
  SUPPORTED_TABLE_DESIGNER_TYPES,
229
322
  buildTableDesignerDraft,
323
+ buildComplexUniqueConstraints,
324
+ buildCheckConstraints,
230
325
  createDesignerWarning,
231
326
  listDesignerTables,
232
327
  normalizeDesignerType,
@@ -66,6 +66,53 @@ function normalizeColumnPayload(column = {}, index = 0) {
66
66
  };
67
67
  }
68
68
 
69
+ function normalizeUniqueConstraintPayload(constraint = {}, index = 0) {
70
+ return {
71
+ id: String(constraint.id ?? `unique:${index}`),
72
+ name: String(constraint.name ?? "").trim(),
73
+ originalName: String(constraint.originalName ?? constraint.name ?? "").trim(),
74
+ columns: Array.isArray(constraint.columns)
75
+ ? constraint.columns
76
+ .map((column) => ({
77
+ name: String(column?.name ?? "").trim(),
78
+ descending: normalizeBoolean(column?.descending),
79
+ collation: String(column?.collation ?? "").trim(),
80
+ }))
81
+ .filter((column) => column.name)
82
+ : [],
83
+ partial: normalizeBoolean(constraint.partial),
84
+ origin: String(constraint.origin ?? "").trim(),
85
+ sql: String(constraint.sql ?? "").trim(),
86
+ originalSql: String(constraint.originalSql ?? constraint.sql ?? "").trim(),
87
+ expression: String(constraint.expression ?? "").trim(),
88
+ originalExpression: String(constraint.originalExpression ?? constraint.expression ?? constraint.sql ?? "").trim(),
89
+ editable: normalizeBoolean(constraint.editable),
90
+ preserved: constraint.preserved === undefined ? true : normalizeBoolean(constraint.preserved),
91
+ };
92
+ }
93
+
94
+ function normalizeCheckConstraintPayload(constraint = {}, index = 0) {
95
+ return {
96
+ id: String(constraint.id ?? `check:${index}`),
97
+ name: String(constraint.name ?? `CHECK ${index + 1}`).trim(),
98
+ originalName: String(constraint.originalName ?? constraint.name ?? `CHECK ${index + 1}`).trim(),
99
+ columns: Array.isArray(constraint.columns)
100
+ ? constraint.columns
101
+ .map((column) => ({
102
+ name: String(column?.name ?? "").trim(),
103
+ allowedValues: Array.isArray(column?.allowedValues)
104
+ ? column.allowedValues.map((value) => String(value ?? ""))
105
+ : [],
106
+ }))
107
+ .filter((column) => column.name)
108
+ : [],
109
+ expression: String(constraint.expression ?? "").trim(),
110
+ originalExpression: String(constraint.originalExpression ?? constraint.expression ?? "").trim(),
111
+ editable: normalizeBoolean(constraint.editable),
112
+ preserved: constraint.preserved === undefined ? true : normalizeBoolean(constraint.preserved),
113
+ };
114
+ }
115
+
69
116
  function normalizeDraftPayload(payload = {}) {
70
117
  const mode = String(payload.mode ?? "create").trim() === "edit" ? "edit" : "create";
71
118
 
@@ -76,6 +123,17 @@ function normalizeDraftPayload(payload = {}) {
76
123
  columns: Array.isArray(payload.columns)
77
124
  ? payload.columns.map((column, index) => normalizeColumnPayload(column, index))
78
125
  : [],
126
+ uniqueConstraints: Array.isArray(payload.uniqueConstraints)
127
+ ? payload.uniqueConstraints.map((constraint, index) =>
128
+ normalizeUniqueConstraintPayload(constraint, index)
129
+ )
130
+ : [],
131
+ checkConstraints: Array.isArray(payload.checkConstraints)
132
+ ? payload.checkConstraints.map((constraint, index) =>
133
+ normalizeCheckConstraintPayload(constraint, index)
134
+ )
135
+ : [],
136
+ designerVersion: Number(payload.designerVersion) || 1,
79
137
  schemaWarnings: Array.isArray(payload.schemaWarnings) ? payload.schemaWarnings : [],
80
138
  warnings: Array.isArray(payload.warnings) ? payload.warnings : [],
81
139
  fillImportedRows: normalizeBoolean(payload.fillImportedRows),
@@ -112,7 +170,7 @@ function validatePrimaryKeys(draft, originalDraft) {
112
170
 
113
171
  if (!originalDraft) {
114
172
  throw new ValidationError(
115
- "Table Designer v1 supports a single primary key column when creating a table."
173
+ "Table Designer v2 supports a single primary key column when creating a table."
116
174
  );
117
175
  }
118
176
 
@@ -134,7 +192,7 @@ function validatePrimaryKeys(draft, originalDraft) {
134
192
 
135
193
  if (!isUnchangedCompositePrimaryKey) {
136
194
  throw new ValidationError(
137
- "Table Designer v1 does not support editing composite primary keys."
195
+ "Table Designer v2 does not support editing composite primary keys."
138
196
  );
139
197
  }
140
198
  }
@@ -109,7 +109,7 @@ class TableDesignerService {
109
109
 
110
110
  if (!analysis.executable) {
111
111
  throw new ValidationError(
112
- "This schema change set would require a SQLite table rebuild. Table Designer v1 keeps the SQL preview available but will not execute those changes automatically.",
112
+ "This schema change set would require a SQLite table rebuild. Table Designer v2 keeps the SQL preview available but will not execute those changes automatically.",
113
113
  {
114
114
  code: "TABLE_DESIGNER_REBUILD_REQUIRED",
115
115
  warnings: analysis.warnings,
@@ -0,0 +1,75 @@
1
+ const { ValidationError } = require("../../utils/errors");
2
+ const { quoteIdentifier } = require("../../utils/identifier");
3
+
4
+ const FILTER_OPERATORS = new Set(["=", "!=", "<", ">", "<=", ">=", "equals"]);
5
+
6
+ function escapeLikePattern(value) {
7
+ return String(value).replace(/[\\%_]/g, (character) => `\\${character}`);
8
+ }
9
+
10
+ function isTextColumn(column) {
11
+ return String(column?.affinity ?? "").toUpperCase() === "TEXT";
12
+ }
13
+
14
+ function normalizeTableFilter(tableDetail, options = {}) {
15
+ const columnName = String(options.filterColumn ?? "").trim();
16
+ const operator = String(options.filterOperator ?? "=").trim();
17
+ const value = options.filterValue;
18
+
19
+ if (!columnName || value === undefined || value === null || String(value).trim() === "") {
20
+ return null;
21
+ }
22
+
23
+ if (!FILTER_OPERATORS.has(operator)) {
24
+ throw new ValidationError(
25
+ `filterOperator must be one of: ${Array.from(FILTER_OPERATORS).join(", ")}.`
26
+ );
27
+ }
28
+
29
+ const filterColumn = tableDetail.columns.find(
30
+ (column) => column.visible && column.name === columnName
31
+ );
32
+
33
+ if (!filterColumn) {
34
+ throw new ValidationError(`Unknown filter column: ${columnName}.`);
35
+ }
36
+
37
+ const normalizedValue = String(value);
38
+ const quotedColumn = quoteIdentifier(filterColumn.name);
39
+
40
+ if (operator === "equals") {
41
+ return {
42
+ column: filterColumn.name,
43
+ operator,
44
+ value: normalizedValue,
45
+ matchMode: "equals",
46
+ clause: `${quotedColumn}${isTextColumn(filterColumn) ? " COLLATE NOCASE" : ""} = ?`,
47
+ params: [normalizedValue],
48
+ };
49
+ }
50
+
51
+ if (isTextColumn(filterColumn) && (operator === "=" || operator === "!=")) {
52
+ return {
53
+ column: filterColumn.name,
54
+ operator,
55
+ value: normalizedValue,
56
+ matchMode: operator === "=" ? "contains" : "notContains",
57
+ clause: `${quotedColumn} COLLATE NOCASE ${operator === "=" ? "LIKE" : "NOT LIKE"} ? ESCAPE '\\'`,
58
+ params: [`%${escapeLikePattern(normalizedValue)}%`],
59
+ };
60
+ }
61
+
62
+ return {
63
+ column: filterColumn.name,
64
+ operator,
65
+ value: normalizedValue,
66
+ matchMode: "comparison",
67
+ clause: `${quotedColumn} ${operator} ?`,
68
+ params: [normalizedValue],
69
+ };
70
+ }
71
+
72
+ module.exports = {
73
+ FILTER_OPERATORS,
74
+ normalizeTableFilter,
75
+ };
@@ -1,10 +1,13 @@
1
- function escapeCsvCell(value, delimiter) {
1
+ function stringifyDelimitedCell(value) {
2
2
  if (value === null || value === undefined) {
3
3
  return "";
4
4
  }
5
5
 
6
- const stringValue =
7
- typeof value === "object" ? JSON.stringify(value) : String(value);
6
+ return typeof value === "object" ? JSON.stringify(value) : String(value);
7
+ }
8
+
9
+ function escapeCsvCell(value, delimiter) {
10
+ const stringValue = stringifyDelimitedCell(value);
8
11
 
9
12
  if (
10
13
  stringValue.includes('"') ||
@@ -18,7 +21,7 @@ function escapeCsvCell(value, delimiter) {
18
21
  return stringValue;
19
22
  }
20
23
 
21
- function rowsToCsv({ columns, rows, delimiter = "," }) {
24
+ function rowsToDelimitedText({ columns, rows, delimiter = "," }) {
22
25
  const header = columns.map((column) => escapeCsvCell(column, delimiter)).join(delimiter);
23
26
  const body = rows.map((row) =>
24
27
  columns
@@ -29,6 +32,29 @@ function rowsToCsv({ columns, rows, delimiter = "," }) {
29
32
  return [header, ...body].join("\n");
30
33
  }
31
34
 
35
+ function rowsToCsv({ columns, rows, delimiter = "," }) {
36
+ return rowsToDelimitedText({ columns, rows, delimiter });
37
+ }
38
+
39
+ function escapeMarkdownCell(value) {
40
+ return stringifyDelimitedCell(value)
41
+ .replaceAll("\\", "\\\\")
42
+ .replaceAll("|", "\\|")
43
+ .replace(/\r\n|\r|\n/g, "<br>");
44
+ }
45
+
46
+ function rowsToMarkdownTable({ columns, rows }) {
47
+ const header = `| ${columns.map(escapeMarkdownCell).join(" | ")} |`;
48
+ const separator = `| ${columns.map(() => "---").join(" | ")} |`;
49
+ const body = rows.map(
50
+ (row) => `| ${columns.map((column) => escapeMarkdownCell(row[column])).join(" | ")} |`
51
+ );
52
+
53
+ return [header, separator, ...body].join("\n");
54
+ }
55
+
32
56
  module.exports = {
33
57
  rowsToCsv,
58
+ rowsToDelimitedText,
59
+ rowsToMarkdownTable,
34
60
  };
@@ -0,0 +1,90 @@
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 { SqlExecutor } = require("../server/services/sqlite/sqlExecutor");
6
+ const { buildTableDesignerDraft } = require("../server/services/sqlite/tableDesigner/schemaMapping");
7
+
8
+ test("table detail exposes string options from simple CHECK IN constraints", () => {
9
+ const db = new Database(":memory:");
10
+
11
+ try {
12
+ db.exec(`
13
+ CREATE TABLE stream_company_mentions (
14
+ id INTEGER PRIMARY KEY,
15
+ mention_type TEXT,
16
+ CHECK (
17
+ mention_type IS NULL
18
+ OR mention_type IN (
19
+ 'company',
20
+ 'stock_company',
21
+ 'brand',
22
+ 'product',
23
+ 'person',
24
+ 'organization',
25
+ 'terrorists',
26
+ 'events',
27
+ 'collective_terms',
28
+ 'countries',
29
+ 'none',
30
+ 'unknown'
31
+ )
32
+ )
33
+ );
34
+ INSERT INTO stream_company_mentions (mention_type) VALUES ('company');
35
+ `);
36
+
37
+ const tableDetail = getTableDetail(db, "stream_company_mentions");
38
+ const designerDraft = buildTableDesignerDraft(tableDetail);
39
+ const mentionTypeColumn = tableDetail.columns.find(
40
+ (column) => column.name === "mention_type"
41
+ );
42
+
43
+ assert.deepEqual(mentionTypeColumn.allowedValues, [
44
+ "company",
45
+ "stock_company",
46
+ "brand",
47
+ "product",
48
+ "person",
49
+ "organization",
50
+ "terrorists",
51
+ "events",
52
+ "collective_terms",
53
+ "countries",
54
+ "none",
55
+ "unknown",
56
+ ]);
57
+
58
+ const executor = new SqlExecutor({
59
+ connectionManager: {
60
+ getActiveDatabase: () => db,
61
+ getActiveConnection: () => ({ id: "test" }),
62
+ },
63
+ appStateStore: {
64
+ recordQueryExecution: () => 1,
65
+ },
66
+ });
67
+ const result = executor.execute(
68
+ "SELECT id, mention_type FROM stream_company_mentions"
69
+ );
70
+ const editableColumn = result.editing.columns.find(
71
+ (column) => column.sourceColumn === "mention_type"
72
+ );
73
+
74
+ assert.deepEqual(editableColumn.allowedValues, mentionTypeColumn.allowedValues);
75
+
76
+ assert.equal(designerDraft.designerVersion, 2);
77
+ assert.equal(designerDraft.checkConstraints.length, 1);
78
+ assert.match(designerDraft.checkConstraints[0].expression, /CHECK/i);
79
+ assert.equal(designerDraft.checkConstraints[0].originalExpression, designerDraft.checkConstraints[0].expression);
80
+ assert.equal(designerDraft.checkConstraints[0].editable, true);
81
+ assert.deepEqual(designerDraft.checkConstraints[0].columns, [
82
+ {
83
+ name: "mention_type",
84
+ allowedValues: mentionTypeColumn.allowedValues,
85
+ },
86
+ ]);
87
+ } finally {
88
+ db.close();
89
+ }
90
+ });
@@ -0,0 +1,34 @@
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 exportFilenamesModulePromise = null;
7
+
8
+ function loadExportFilenamesModule() {
9
+ if (!exportFilenamesModulePromise) {
10
+ const source = readFileSync(
11
+ path.resolve(__dirname, "../frontend/js/utils/exportFilenames.js"),
12
+ "utf8"
13
+ );
14
+ const url = `data:text/javascript;base64,${Buffer.from(source).toString("base64")}`;
15
+
16
+ exportFilenamesModulePromise = import(url);
17
+ }
18
+
19
+ return exportFilenamesModulePromise;
20
+ }
21
+
22
+ test("text export filenames use the selected format extension", async () => {
23
+ const { buildTextExportFilename } = await loadExportFilenamesModule();
24
+
25
+ assert.equal(buildTextExportFilename("query-results.csv", { format: "tsv" }), "query-results.tsv");
26
+ assert.equal(buildTextExportFilename("white_house_live_streams", { format: "md" }), "white_house_live_streams.md");
27
+ });
28
+
29
+ test("text export filenames sanitize unsafe names and fallback when empty", async () => {
30
+ const { buildTextExportFilename } = await loadExportFilenamesModule();
31
+
32
+ assert.equal(buildTextExportFilename("", { format: "csv", fallback: "table" }), "table.csv");
33
+ assert.equal(buildTextExportFilename("../bad:name?.csv", { format: "csv", fallback: "table" }), "bad name.csv");
34
+ });