sqlite-hub 0.12.0 → 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 (46) hide show
  1. package/README.md +12 -4
  2. package/frontend/js/api.js +4 -2
  3. package/frontend/js/app.js +632 -12
  4. package/frontend/js/components/modal.js +432 -3
  5. package/frontend/js/components/queryEditor.js +9 -1
  6. package/frontend/js/components/queryResults.js +79 -17
  7. package/frontend/js/components/rowEditorPanel.js +204 -10
  8. package/frontend/js/components/sidebar.js +101 -18
  9. package/frontend/js/components/tableDesignerEditor.js +69 -11
  10. package/frontend/js/store.js +227 -9
  11. package/frontend/js/utils/copyColumnExport.js +117 -0
  12. package/frontend/js/utils/exportFilenames.js +32 -0
  13. package/frontend/js/utils/filePathPreview.js +315 -0
  14. package/frontend/js/utils/format.js +1 -1
  15. package/frontend/js/utils/rowEditorJson.js +65 -0
  16. package/frontend/js/utils/sqlFormatter.js +691 -0
  17. package/frontend/js/utils/tableDesigner.js +178 -6
  18. package/frontend/js/utils/textCellStats.js +20 -0
  19. package/frontend/js/utils/timestampPreview.js +264 -0
  20. package/frontend/js/views/charts.js +3 -1
  21. package/frontend/js/views/data.js +34 -2
  22. package/frontend/js/views/editor.js +48 -1
  23. package/frontend/js/views/settings.js +0 -3
  24. package/frontend/js/views/structure.js +154 -212
  25. package/frontend/styles/base.css +6 -0
  26. package/frontend/styles/components.css +463 -2
  27. package/frontend/styles/structure-graph.css +0 -3
  28. package/frontend/styles/tailwind.generated.css +1 -1
  29. package/frontend/styles/tokens.css +1 -1
  30. package/package.json +2 -3
  31. package/server/services/sqlite/introspection.js +10 -0
  32. package/server/services/sqlite/sqlExecutor.js +29 -0
  33. package/server/services/sqlite/tableDesigner/changeAnalysis.js +25 -1
  34. package/server/services/sqlite/tableDesigner/schemaMapping.js +105 -10
  35. package/server/services/sqlite/tableDesigner/validation.js +60 -2
  36. package/server/services/sqlite/tableDesignerService.js +1 -1
  37. package/tests/check-constraint-options.test.js +14 -0
  38. package/tests/export-filenames.test.js +34 -0
  39. package/tests/file-path-preview.test.js +165 -0
  40. package/tests/row-editor-json.test.js +82 -0
  41. package/tests/row-editor-timestamp-preview.test.js +192 -0
  42. package/tests/sql-formatter.test.js +173 -0
  43. package/tests/sql-highlight.test.js +38 -0
  44. package/tests/table-designer-v2-unique-constraints.test.js +78 -0
  45. package/tests/text-cell-stats.test.js +38 -0
  46. package/fill.js +0 -526
@@ -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,
@@ -3,6 +3,7 @@ const assert = require("node:assert/strict");
3
3
  const test = require("node:test");
4
4
  const { getTableDetail } = require("../server/services/sqlite/introspection");
5
5
  const { SqlExecutor } = require("../server/services/sqlite/sqlExecutor");
6
+ const { buildTableDesignerDraft } = require("../server/services/sqlite/tableDesigner/schemaMapping");
6
7
 
7
8
  test("table detail exposes string options from simple CHECK IN constraints", () => {
8
9
  const db = new Database(":memory:");
@@ -34,6 +35,7 @@ test("table detail exposes string options from simple CHECK IN constraints", ()
34
35
  `);
35
36
 
36
37
  const tableDetail = getTableDetail(db, "stream_company_mentions");
38
+ const designerDraft = buildTableDesignerDraft(tableDetail);
37
39
  const mentionTypeColumn = tableDetail.columns.find(
38
40
  (column) => column.name === "mention_type"
39
41
  );
@@ -70,6 +72,18 @@ test("table detail exposes string options from simple CHECK IN constraints", ()
70
72
  );
71
73
 
72
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
+ ]);
73
87
  } finally {
74
88
  db.close();
75
89
  }
@@ -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
+ });
@@ -0,0 +1,165 @@
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 filePathPreviewModulePromise = null;
7
+
8
+ function loadFilePathPreviewModule() {
9
+ if (!filePathPreviewModulePromise) {
10
+ const timestampPreviewSource = readFileSync(
11
+ path.resolve(__dirname, "../frontend/js/utils/timestampPreview.js"),
12
+ "utf8"
13
+ );
14
+ const timestampPreviewUrl = `data:text/javascript;base64,${Buffer.from(timestampPreviewSource).toString("base64")}`;
15
+ const filePathPreviewSource = readFileSync(
16
+ path.resolve(__dirname, "../frontend/js/utils/filePathPreview.js"),
17
+ "utf8"
18
+ ).replace(
19
+ 'import { isProtectedKeyColumn } from "./timestampPreview.js";',
20
+ `import { isProtectedKeyColumn } from "${timestampPreviewUrl}";`
21
+ );
22
+ const filePathPreviewUrl = `data:text/javascript;base64,${Buffer.from(filePathPreviewSource).toString("base64")}`;
23
+
24
+ filePathPreviewModulePromise = import(filePathPreviewUrl);
25
+ }
26
+
27
+ return filePathPreviewModulePromise;
28
+ }
29
+
30
+ function createTableMeta() {
31
+ return {
32
+ columns: [
33
+ { name: "id", primaryKeyPosition: 1 },
34
+ { name: "parent_id", primaryKeyPosition: 0 },
35
+ { name: "file_path", primaryKeyPosition: 0 },
36
+ { name: "filename", primaryKeyPosition: 0 },
37
+ { name: "note", primaryKeyPosition: 0 },
38
+ ],
39
+ foreignKeys: [
40
+ {
41
+ mappings: [{ from: "parent_id", to: "id" }],
42
+ },
43
+ ],
44
+ };
45
+ }
46
+
47
+ test("filepath preview detects supported path styles", async () => {
48
+ const { detectFilePathValue } = await loadFilePathPreviewModule();
49
+ const tableMeta = createTableMeta();
50
+ const cases = [
51
+ {
52
+ value: "/Users/oli/project/file.txt",
53
+ pathType: "unix",
54
+ fileName: "file.txt",
55
+ directory: "/Users/oli/project",
56
+ extension: "txt",
57
+ },
58
+ {
59
+ value: "/home/oli/data/app.sqlite",
60
+ pathType: "unix",
61
+ fileName: "app.sqlite",
62
+ directory: "/home/oli/data",
63
+ extension: "sqlite",
64
+ },
65
+ {
66
+ value: "C:\\Users\\Oliver\\Desktop\\file.pdf",
67
+ pathType: "windows",
68
+ fileName: "file.pdf",
69
+ directory: "C:\\Users\\Oliver\\Desktop",
70
+ extension: "pdf",
71
+ },
72
+ {
73
+ value: "./data/export.csv",
74
+ pathType: "relative",
75
+ fileName: "export.csv",
76
+ directory: "./data",
77
+ extension: "csv",
78
+ },
79
+ {
80
+ value: "../logs/app.log",
81
+ pathType: "relative",
82
+ fileName: "app.log",
83
+ directory: "../logs",
84
+ extension: "log",
85
+ },
86
+ {
87
+ value: "~/Documents/report.md",
88
+ pathType: "home",
89
+ fileName: "report.md",
90
+ directory: "~/Documents",
91
+ extension: "md",
92
+ },
93
+ {
94
+ value: "data/images/avatar.png",
95
+ pathType: "relative",
96
+ fileName: "avatar.png",
97
+ directory: "data/images",
98
+ extension: "png",
99
+ },
100
+ ];
101
+
102
+ for (const item of cases) {
103
+ const preview = detectFilePathValue(item.value, "file_path", tableMeta);
104
+
105
+ assert.equal(preview?.type, "filepath");
106
+ assert.equal(preview.pathType, item.pathType);
107
+ assert.equal(preview.fileName, item.fileName);
108
+ assert.equal(preview.directory, item.directory);
109
+ assert.equal(preview.extension, item.extension);
110
+ assert.ok(preview.confidence >= 0.7);
111
+ }
112
+ });
113
+
114
+ test("filepath preview does not detect URLs, primitive text, JSON, emails, or weak filenames", async () => {
115
+ const { detectFilePathValue } = await loadFilePathPreviewModule();
116
+ const tableMeta = createTableMeta();
117
+ const rejectedValues = [
118
+ "https://example.com/file.txt",
119
+ "http://localhost:3000/test",
120
+ "mailto:oliver@example.com",
121
+ "tel:+431234567",
122
+ "ftp://example.com/file.txt",
123
+ "oliver@example.com",
124
+ "1717682400",
125
+ "true",
126
+ "false",
127
+ '{"path":"/tmp/file.txt"}',
128
+ "hello world",
129
+ "file",
130
+ "invoice.pdf",
131
+ ];
132
+
133
+ for (const value of rejectedValues) {
134
+ assert.equal(detectFilePathValue(value, "note", tableMeta), null);
135
+ }
136
+ });
137
+
138
+ test("filepath preview protects primary keys and foreign keys", async () => {
139
+ const { detectFilePathValue } = await loadFilePathPreviewModule();
140
+ const tableMeta = createTableMeta();
141
+
142
+ assert.equal(detectFilePathValue("/Users/oli/project/file.txt", "id", tableMeta), null);
143
+ assert.equal(detectFilePathValue("/Users/oli/project/file.txt", "parent_id", tableMeta), null);
144
+ });
145
+
146
+ test("filepath preview allows bare filenames only for strong file columns", async () => {
147
+ const { detectFilePathValue } = await loadFilePathPreviewModule();
148
+ const tableMeta = createTableMeta();
149
+
150
+ assert.equal(detectFilePathValue("invoice.pdf", "note", tableMeta), null);
151
+
152
+ const preview = detectFilePathValue("invoice.pdf", "filename", tableMeta);
153
+
154
+ assert.equal(preview?.pathType, "relative");
155
+ assert.equal(preview.fileName, "invoice.pdf");
156
+ assert.equal(preview.directory, null);
157
+ assert.equal(preview.extension, "pdf");
158
+ });
159
+
160
+ test("filepath preview compacts long paths while preserving filename", async () => {
161
+ const { compactPathForDisplay } = await loadFilePathPreviewModule();
162
+ const display = compactPathForDisplay("/Users/oli/projects/sqlite-hub/data/app.sqlite", 24);
163
+
164
+ assert.equal(display, ".../data/app.sqlite");
165
+ });
@@ -0,0 +1,82 @@
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 rowEditorJsonModulePromise = null;
7
+
8
+ function loadRowEditorJsonModule() {
9
+ if (!rowEditorJsonModulePromise) {
10
+ const source = readFileSync(
11
+ path.resolve(__dirname, "../frontend/js/utils/rowEditorJson.js"),
12
+ "utf8"
13
+ );
14
+ const url = `data:text/javascript;base64,${Buffer.from(source).toString("base64")}`;
15
+
16
+ rowEditorJsonModulePromise = import(url);
17
+ }
18
+
19
+ return rowEditorJsonModulePromise;
20
+ }
21
+
22
+ test("row editor JSON for data rows preserves column order and omits identity metadata", async () => {
23
+ const { buildDataRowEditorJsonObject, stringifyRowEditorJson } = await loadRowEditorJsonModule();
24
+ const row = {
25
+ id: 1,
26
+ title: "Event",
27
+ created_at: 1717682400,
28
+ payload: { __type: "blob", sizeBytes: 4, hexPreview: "ffff" },
29
+ __identity: { kind: "primaryKey", values: { id: 1 } },
30
+ };
31
+ const rowObject = buildDataRowEditorJsonObject({
32
+ row,
33
+ columns: ["id", "title", "created_at", "payload"],
34
+ });
35
+
36
+ assert.deepEqual(Object.keys(rowObject), ["id", "title", "created_at", "payload"]);
37
+ assert.deepEqual(rowObject, {
38
+ id: 1,
39
+ title: "Event",
40
+ created_at: 1717682400,
41
+ payload: { __type: "blob", sizeBytes: 4, hexPreview: "ffff" },
42
+ });
43
+ assert.equal(
44
+ stringifyRowEditorJson(rowObject),
45
+ [
46
+ "{",
47
+ ' "id": 1,',
48
+ ' "title": "Event",',
49
+ ' "created_at": 1717682400,',
50
+ ' "payload": {',
51
+ ' "__type": "blob",',
52
+ ' "sizeBytes": 4,',
53
+ ' "hexPreview": "ffff"',
54
+ " }",
55
+ "}",
56
+ ].join("\n")
57
+ );
58
+ });
59
+
60
+ test("row editor JSON for SQL result rows maps visible source columns once", async () => {
61
+ const { buildEditorRowEditorJsonObject } = await loadRowEditorJsonModule();
62
+ const rowObject = buildEditorRowEditorJsonObject({
63
+ row: {
64
+ id: 1,
65
+ title_alias: "Event",
66
+ duplicate_title: "Ignored",
67
+ hidden_note: "Hidden",
68
+ __identity: { kind: "primaryKey", values: { id: 1 } },
69
+ },
70
+ editingColumns: [
71
+ { resultName: "id", sourceColumn: "id", visible: true },
72
+ { resultName: "title_alias", sourceColumn: "title", visible: true },
73
+ { resultName: "duplicate_title", sourceColumn: "title", visible: true },
74
+ { resultName: "hidden_note", sourceColumn: "note", visible: false },
75
+ ],
76
+ });
77
+
78
+ assert.deepEqual(rowObject, {
79
+ id: 1,
80
+ title: "Event",
81
+ });
82
+ });
@@ -0,0 +1,192 @@
1
+ const Database = require("better-sqlite3");
2
+ const assert = require("node:assert/strict");
3
+ const { readFileSync } = require("node:fs");
4
+ const path = require("node:path");
5
+ const test = require("node:test");
6
+ const { DataBrowserService } = require("../server/services/sqlite/dataBrowserService");
7
+
8
+ let timestampPreviewModulePromise = null;
9
+
10
+ function loadTimestampPreviewModule() {
11
+ if (!timestampPreviewModulePromise) {
12
+ const source = readFileSync(
13
+ path.resolve(__dirname, "../frontend/js/utils/timestampPreview.js"),
14
+ "utf8"
15
+ );
16
+ const url = `data:text/javascript;base64,${Buffer.from(source).toString("base64")}`;
17
+
18
+ timestampPreviewModulePromise = import(url);
19
+ }
20
+
21
+ return timestampPreviewModulePromise;
22
+ }
23
+
24
+ function createTableMeta() {
25
+ return {
26
+ columns: [
27
+ { name: "id", primaryKeyPosition: 1 },
28
+ { name: "parent_id", primaryKeyPosition: 0 },
29
+ { name: "created_at", primaryKeyPosition: 0 },
30
+ { name: "updated_at", primaryKeyPosition: 0 },
31
+ { name: "published_at", primaryKeyPosition: 0 },
32
+ { name: "price", primaryKeyPosition: 0 },
33
+ ],
34
+ foreignKeys: [
35
+ {
36
+ mappings: [{ from: "parent_id", to: "id" }],
37
+ },
38
+ ],
39
+ };
40
+ }
41
+
42
+ test("row editor timestamp preview protects primary and foreign keys", async () => {
43
+ const {
44
+ getTimestampPreviewForField,
45
+ isProtectedKeyColumn,
46
+ } = await loadTimestampPreviewModule();
47
+ const tableMeta = createTableMeta();
48
+
49
+ assert.equal(isProtectedKeyColumn("id", tableMeta), true);
50
+ assert.equal(isProtectedKeyColumn("parent_id", tableMeta), true);
51
+
52
+ assert.equal(
53
+ getTimestampPreviewForField({
54
+ columnName: "id",
55
+ value: "1717682400",
56
+ tableMeta,
57
+ }).kind,
58
+ "protected-key"
59
+ );
60
+ assert.equal(
61
+ getTimestampPreviewForField({
62
+ columnName: "parent_id",
63
+ value: "1717682400",
64
+ tableMeta,
65
+ }).kind,
66
+ "protected-key"
67
+ );
68
+ });
69
+
70
+ test("row editor timestamp preview formats plausible non-key timestamp values", async () => {
71
+ const { getTimestampPreviewForField } = await loadTimestampPreviewModule();
72
+ const tableMeta = createTableMeta();
73
+
74
+ const createdAtPreview = getTimestampPreviewForField({
75
+ columnName: "created_at",
76
+ value: "1717682400",
77
+ tableMeta,
78
+ });
79
+ const updatedAtPreview = getTimestampPreviewForField({
80
+ columnName: "updated_at",
81
+ value: "1717682400000",
82
+ tableMeta,
83
+ });
84
+ const pricePreview = getTimestampPreviewForField({
85
+ columnName: "price",
86
+ value: "1717682400",
87
+ tableMeta,
88
+ });
89
+ const publishedAtPreview = getTimestampPreviewForField({
90
+ columnName: "published_at",
91
+ value: "1717682400000000",
92
+ tableMeta,
93
+ });
94
+
95
+ assert.equal(createdAtPreview.kind, "timestamp");
96
+ assert.equal(createdAtPreview.sourceFormat, "unix-seconds");
97
+ assert.equal(updatedAtPreview.kind, "timestamp");
98
+ assert.equal(updatedAtPreview.sourceFormat, "unix-milliseconds");
99
+ assert.equal(publishedAtPreview.kind, "timestamp");
100
+ assert.equal(publishedAtPreview.sourceFormat, "unix-microseconds");
101
+ assert.equal(pricePreview.kind, "timestamp");
102
+ assert.match(createdAtPreview.formatted, /^\d{2}\.\d{2}\.\d{4}, \d{2}:\d{2}:\d{2}$/);
103
+ });
104
+
105
+ test("row editor timestamp preview ignores ids and invalid timestamps", async () => {
106
+ const { getTimestampPreviewForField } = await loadTimestampPreviewModule();
107
+ const tableMeta = createTableMeta();
108
+
109
+ assert.equal(
110
+ getTimestampPreviewForField({
111
+ columnName: "external_id",
112
+ value: "1717682400",
113
+ tableMeta,
114
+ }).kind,
115
+ "none"
116
+ );
117
+ assert.equal(
118
+ getTimestampPreviewForField({
119
+ columnName: "created_at",
120
+ value: "42",
121
+ tableMeta,
122
+ }).kind,
123
+ "none"
124
+ );
125
+ assert.equal(
126
+ getTimestampPreviewForField({
127
+ columnName: "created_at",
128
+ value: "not a timestamp",
129
+ tableMeta,
130
+ }).kind,
131
+ "none"
132
+ );
133
+ });
134
+
135
+ test("row editor timestamp preview accepts ISO and SQLite datetime strings", async () => {
136
+ const { getTimestampPreviewForField } = await loadTimestampPreviewModule();
137
+ const tableMeta = createTableMeta();
138
+
139
+ assert.equal(
140
+ getTimestampPreviewForField({
141
+ columnName: "created_at",
142
+ value: "2026-06-06T18:49:00Z",
143
+ tableMeta,
144
+ }).kind,
145
+ "timestamp"
146
+ );
147
+ assert.equal(
148
+ getTimestampPreviewForField({
149
+ columnName: "updated_at",
150
+ value: "2026-06-06 18:49:00",
151
+ tableMeta,
152
+ }).kind,
153
+ "timestamp"
154
+ );
155
+ });
156
+
157
+ test("row update stores the submitted raw value without date conversion", () => {
158
+ const db = new Database(":memory:");
159
+
160
+ try {
161
+ db.exec(`
162
+ CREATE TABLE events (
163
+ id INTEGER PRIMARY KEY,
164
+ created_at TEXT
165
+ );
166
+ INSERT INTO events (id, created_at) VALUES (1, 'before');
167
+ `);
168
+
169
+ const service = new DataBrowserService({
170
+ connectionManager: {
171
+ assertWritable() {},
172
+ getActiveDatabase: () => db,
173
+ },
174
+ });
175
+ const tableData = service.getTableData("events", { limit: 10, offset: 0 });
176
+ const identity = tableData.rows[0].__identity;
177
+
178
+ service.updateTableRow("events", {
179
+ identity,
180
+ values: {
181
+ created_at: "1717682400",
182
+ },
183
+ });
184
+
185
+ assert.equal(
186
+ db.prepare("SELECT created_at FROM events WHERE id = 1").get().created_at,
187
+ "1717682400"
188
+ );
189
+ } finally {
190
+ db.close();
191
+ }
192
+ });