sqlite-hub 0.12.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +118 -23
  2. package/bin/sqlite-hub.js +1041 -224
  3. package/frontend/index.html +1 -0
  4. package/frontend/js/api.js +32 -2
  5. package/frontend/js/app.js +989 -13
  6. package/frontend/js/components/modal.js +644 -15
  7. package/frontend/js/components/pageHeader.js +14 -16
  8. package/frontend/js/components/queryEditor.js +9 -1
  9. package/frontend/js/components/queryHistoryPanel.js +126 -131
  10. package/frontend/js/components/queryResults.js +79 -17
  11. package/frontend/js/components/rowEditorPanel.js +204 -10
  12. package/frontend/js/components/sidebar.js +102 -18
  13. package/frontend/js/components/tableDesignerEditor.js +69 -11
  14. package/frontend/js/router.js +8 -0
  15. package/frontend/js/store.js +868 -9
  16. package/frontend/js/utils/copyColumnExport.js +117 -0
  17. package/frontend/js/utils/exportFilenames.js +32 -0
  18. package/frontend/js/utils/filePathPreview.js +315 -0
  19. package/frontend/js/utils/format.js +1 -1
  20. package/frontend/js/utils/markdownDocuments.js +248 -0
  21. package/frontend/js/utils/rowEditorJson.js +65 -0
  22. package/frontend/js/utils/sqlFormatter.js +691 -0
  23. package/frontend/js/utils/tableDesigner.js +178 -6
  24. package/frontend/js/utils/textCellStats.js +20 -0
  25. package/frontend/js/utils/timestampPreview.js +264 -0
  26. package/frontend/js/views/charts.js +3 -1
  27. package/frontend/js/views/data.js +34 -2
  28. package/frontend/js/views/documents.js +300 -0
  29. package/frontend/js/views/editor.js +48 -1
  30. package/frontend/js/views/settings.js +39 -6
  31. package/frontend/js/views/structure.js +154 -212
  32. package/frontend/styles/base.css +6 -0
  33. package/frontend/styles/components.css +476 -2
  34. package/frontend/styles/structure-graph.css +0 -3
  35. package/frontend/styles/tailwind.generated.css +1 -1
  36. package/frontend/styles/tokens.css +1 -1
  37. package/frontend/styles/views.css +422 -0
  38. package/package.json +3 -3
  39. package/server/routes/documents.js +163 -0
  40. package/server/routes/settings.js +22 -6
  41. package/server/server.js +6 -0
  42. package/server/services/sqlite/introspection.js +10 -0
  43. package/server/services/sqlite/sqlExecutor.js +29 -0
  44. package/server/services/sqlite/tableDesigner/changeAnalysis.js +25 -1
  45. package/server/services/sqlite/tableDesigner/schemaMapping.js +105 -10
  46. package/server/services/sqlite/tableDesigner/validation.js +60 -2
  47. package/server/services/sqlite/tableDesignerService.js +1 -1
  48. package/server/services/storage/appStateStore.js +313 -0
  49. package/tests/check-constraint-options.test.js +14 -0
  50. package/tests/cli-args.test.js +100 -0
  51. package/tests/copy-column-modal.test.js +83 -0
  52. package/tests/database-documents.test.js +85 -0
  53. package/tests/export-filenames.test.js +34 -0
  54. package/tests/file-path-preview.test.js +165 -0
  55. package/tests/markdown-documents.test.js +79 -0
  56. package/tests/row-editor-json.test.js +82 -0
  57. package/tests/row-editor-timestamp-preview.test.js +192 -0
  58. package/tests/settings-metadata.test.js +16 -0
  59. package/tests/settings-view.test.js +32 -0
  60. package/tests/sql-formatter.test.js +173 -0
  61. package/tests/sql-highlight.test.js +38 -0
  62. package/tests/table-designer-v2-unique-constraints.test.js +78 -0
  63. package/tests/text-cell-stats.test.js +38 -0
  64. package/fill.js +0 -526
@@ -103,6 +103,52 @@ function normalizeDraft(rawDraft = {}) {
103
103
  columns: Array.isArray(rawDraft.columns)
104
104
  ? rawDraft.columns.map((column) => createEmptyTableDesignerColumn(column))
105
105
  : [],
106
+ uniqueConstraints: Array.isArray(rawDraft.uniqueConstraints)
107
+ ? rawDraft.uniqueConstraints.map((constraint, index) => ({
108
+ id: normalizeText(constraint.id ?? `unique:${index}`),
109
+ name: normalizeText(constraint.name ?? ""),
110
+ originalName: normalizeText(constraint.originalName ?? constraint.name ?? ""),
111
+ columns: Array.isArray(constraint.columns)
112
+ ? constraint.columns
113
+ .map((column) => ({
114
+ name: normalizeText(column?.name ?? ""),
115
+ descending: normalizeBoolean(column?.descending),
116
+ collation: normalizeText(column?.collation ?? ""),
117
+ }))
118
+ .filter((column) => normalizeTrimmed(column.name))
119
+ : [],
120
+ partial: normalizeBoolean(constraint.partial),
121
+ origin: normalizeText(constraint.origin ?? ""),
122
+ sql: normalizeText(constraint.sql ?? ""),
123
+ originalSql: normalizeText(constraint.originalSql ?? constraint.sql ?? ""),
124
+ expression: normalizeText(constraint.expression ?? ""),
125
+ originalExpression: normalizeText(constraint.originalExpression ?? constraint.expression ?? constraint.sql ?? ""),
126
+ editable: normalizeBoolean(constraint.editable),
127
+ preserved: constraint.preserved === undefined ? true : normalizeBoolean(constraint.preserved),
128
+ }))
129
+ : [],
130
+ checkConstraints: Array.isArray(rawDraft.checkConstraints)
131
+ ? rawDraft.checkConstraints.map((constraint, index) => ({
132
+ id: normalizeText(constraint.id ?? `check:${index}`),
133
+ name: normalizeText(constraint.name ?? `CHECK ${index + 1}`),
134
+ originalName: normalizeText(constraint.originalName ?? constraint.name ?? `CHECK ${index + 1}`),
135
+ columns: Array.isArray(constraint.columns)
136
+ ? constraint.columns
137
+ .map((column) => ({
138
+ name: normalizeText(column?.name ?? ""),
139
+ allowedValues: Array.isArray(column?.allowedValues)
140
+ ? column.allowedValues.map((value) => normalizeText(value))
141
+ : [],
142
+ }))
143
+ .filter((column) => normalizeTrimmed(column.name))
144
+ : [],
145
+ expression: normalizeText(constraint.expression ?? ""),
146
+ originalExpression: normalizeText(constraint.originalExpression ?? constraint.expression ?? ""),
147
+ editable: normalizeBoolean(constraint.editable),
148
+ preserved: constraint.preserved === undefined ? true : normalizeBoolean(constraint.preserved),
149
+ }))
150
+ : [],
151
+ designerVersion: Number(rawDraft.designerVersion) || 1,
106
152
  schemaWarnings: Array.isArray(rawDraft.schemaWarnings) ? rawDraft.schemaWarnings : [],
107
153
  fillImportedRows: normalizeBoolean(rawDraft.fillImportedRows),
108
154
  importedCsvFileName: normalizeText(rawDraft.importedCsvFileName ?? ""),
@@ -164,7 +210,7 @@ function validatePrimaryKeys(draft) {
164
210
  }
165
211
 
166
212
  if (draft.mode !== "edit") {
167
- return ["Table Designer v1 supports only one primary key column for new tables."];
213
+ return ["Table Designer v2 supports only one primary key column for new tables."];
168
214
  }
169
215
 
170
216
  const originalPrimaryKeyNames = new Set(
@@ -185,7 +231,7 @@ function validatePrimaryKeys(draft) {
185
231
 
186
232
  return isUnchangedCompositePrimaryKey
187
233
  ? []
188
- : ["Composite primary keys can be preserved but not edited in Table Designer v1."];
234
+ : ["Composite primary keys can be preserved but not edited in Table Designer v2."];
189
235
  }
190
236
 
191
237
  function resolveReferencedTableColumns(draft, catalogTables, referencedTableName) {
@@ -330,6 +376,17 @@ function hasColumnChanged(originalColumn, draftColumn) {
330
376
  );
331
377
  }
332
378
 
379
+ function hasConstraintChanged(constraint) {
380
+ return (
381
+ normalizeComparableValue(constraint.name) !==
382
+ normalizeComparableValue(constraint.originalName ?? constraint.name) ||
383
+ normalizeComparableValue(constraint.expression || constraint.sql) !==
384
+ normalizeComparableValue(
385
+ constraint.originalExpression ?? constraint.originalSql ?? constraint.expression ?? constraint.sql
386
+ )
387
+ );
388
+ }
389
+
333
390
  function buildColumnDefinition(column) {
334
391
  const parts = [quoteIdentifier(column.name)];
335
392
  const type = normalizeComparableValue(column.type);
@@ -373,6 +430,24 @@ function buildCreateTableSql(draft) {
373
430
  return `CREATE TABLE ${quoteIdentifier(draft.tableName)} (\n${columnSql}\n);`;
374
431
  }
375
432
 
433
+ function getUniqueConstraintExpression(constraint) {
434
+ const expression = normalizeTrimmed(constraint.expression || constraint.sql);
435
+
436
+ if (expression) {
437
+ return expression;
438
+ }
439
+
440
+ const columnSql = (constraint.columns ?? [])
441
+ .map((column) => quoteIdentifier(column.name))
442
+ .join(", ");
443
+
444
+ return columnSql ? `UNIQUE (${columnSql})` : "UNIQUE constraint";
445
+ }
446
+
447
+ function getCheckConstraintExpression(constraint) {
448
+ return normalizeTrimmed(constraint.expression) || "CHECK constraint";
449
+ }
450
+
376
451
  function buildAlterTableRenameSql(fromName, toName) {
377
452
  return `ALTER TABLE ${quoteIdentifier(fromName)} RENAME TO ${quoteIdentifier(toName)};`;
378
453
  }
@@ -484,7 +559,7 @@ function analyzeEditDraft(draft) {
484
559
  warnings.push(
485
560
  buildRiskyChangeWarning(
486
561
  "Column Rename Requires Rebuild",
487
- `Renaming column ${originalColumn.name} to ${column.name} is intentionally blocked in Table Designer v1.`
562
+ `Renaming column ${originalColumn.name} to ${column.name} is intentionally blocked in Table Designer v2.`
488
563
  )
489
564
  );
490
565
  }
@@ -580,6 +655,19 @@ function analyzeEditDraft(draft) {
580
655
  );
581
656
  });
582
657
 
658
+ [...(draft.uniqueConstraints ?? []), ...(draft.checkConstraints ?? [])].forEach((constraint) => {
659
+ if (!hasConstraintChanged(constraint)) {
660
+ return;
661
+ }
662
+
663
+ warnings.push(
664
+ buildRiskyChangeWarning(
665
+ "Constraint Change Requires Rebuild",
666
+ `Changing ${constraint.originalName || constraint.name || "a table constraint"} requires a SQLite table rebuild.`
667
+ )
668
+ );
669
+ });
670
+
583
671
  return {
584
672
  dirty: Boolean(executableStatements.length || warnings.length),
585
673
  executable: warnings.length === 0,
@@ -611,10 +699,43 @@ function prefixSqlAsComment(sql) {
611
699
  }
612
700
 
613
701
  function buildPreviewSql({ draft, validationErrors, warnings, analysis, readOnly }) {
702
+ const preservedUniqueConstraints = draft.mode === "edit" ? draft.uniqueConstraints ?? [] : [];
703
+ const preservedCheckConstraints = draft.mode === "edit" ? draft.checkConstraints ?? [] : [];
704
+ const preservedUniqueConstraintComments = preservedUniqueConstraints.length
705
+ ? [
706
+ "-- Table Designer v2 preserves these UNIQUE constraints:",
707
+ ...preservedUniqueConstraints.map(
708
+ (constraint) => `-- - ${getUniqueConstraintExpression(constraint)}`
709
+ ),
710
+ ]
711
+ : [];
712
+ const preservedCheckConstraintComments = preservedCheckConstraints.length
713
+ ? [
714
+ "-- Table Designer v2 preserves these CHECK constraints:",
715
+ ...preservedCheckConstraints.map(
716
+ (constraint) => `-- - ${getCheckConstraintExpression(constraint)}`
717
+ ),
718
+ ]
719
+ : [];
720
+ const preservedConstraintComments = [
721
+ ...preservedUniqueConstraintComments,
722
+ ...(
723
+ preservedUniqueConstraintComments.length && preservedCheckConstraintComments.length
724
+ ? [""]
725
+ : []
726
+ ),
727
+ ...preservedCheckConstraintComments,
728
+ ];
729
+
614
730
  if (readOnly) {
615
731
  return [
616
732
  "-- This connection is opened READ ONLY.",
617
733
  "-- Table Designer preview is available, but schema changes cannot be saved.",
734
+ ...(
735
+ preservedConstraintComments.length
736
+ ? ["", ...preservedConstraintComments]
737
+ : []
738
+ ),
618
739
  ].join("\n");
619
740
  }
620
741
 
@@ -627,7 +748,7 @@ function buildPreviewSql({ draft, validationErrors, warnings, analysis, readOnly
627
748
 
628
749
  if (warnings.some((warning) => warning.blocking)) {
629
750
  const lines = [
630
- "-- This draft is not executable in Table Designer v1.",
751
+ "-- This draft is not executable in Table Designer v2.",
631
752
  "-- SQLite would require a table rebuild for the requested changes:",
632
753
  ...warnings
633
754
  .filter((warning) => warning.blocking)
@@ -641,6 +762,10 @@ function buildPreviewSql({ draft, validationErrors, warnings, analysis, readOnly
641
762
  });
642
763
  }
643
764
 
765
+ if (preservedConstraintComments.length) {
766
+ lines.push("", ...preservedConstraintComments);
767
+ }
768
+
644
769
  lines.push(
645
770
  "",
646
771
  "-- Suggested rebuild outline:",
@@ -656,10 +781,25 @@ function buildPreviewSql({ draft, validationErrors, warnings, analysis, readOnly
656
781
  }
657
782
 
658
783
  if (!analysis.dirty && draft.mode === "edit") {
659
- return "-- No schema changes pending.";
784
+ return [
785
+ "-- No schema changes pending.",
786
+ ...(
787
+ preservedConstraintComments.length
788
+ ? ["", ...preservedConstraintComments]
789
+ : []
790
+ ),
791
+ ].join("\n");
660
792
  }
661
793
 
662
- return [...analysis.statements, ...buildImportedInsertPreviewSql(draft)].join("\n\n");
794
+ return [
795
+ ...analysis.statements,
796
+ ...(
797
+ preservedConstraintComments.length
798
+ ? [preservedConstraintComments.join("\n")]
799
+ : []
800
+ ),
801
+ ...buildImportedInsertPreviewSql(draft),
802
+ ].join("\n\n");
663
803
  }
664
804
 
665
805
  function escapeSqlLiteral(value) {
@@ -1029,6 +1169,9 @@ export function createTableDesignerDraftFromCsvImport(
1029
1169
  originalTableName: "",
1030
1170
  tableName: suggestImportedTableName(fileName, catalogTables),
1031
1171
  columns,
1172
+ uniqueConstraints: [],
1173
+ checkConstraints: [],
1174
+ designerVersion: 2,
1032
1175
  schemaWarnings: [],
1033
1176
  fillImportedRows: importedCsvRows.length > 0,
1034
1177
  importedCsvFileName: normalizeText(fileName),
@@ -1053,6 +1196,9 @@ export function createNewTableDesignerDraft() {
1053
1196
  originalTableName: "",
1054
1197
  tableName: "",
1055
1198
  columns: [createEmptyTableDesignerColumn()],
1199
+ uniqueConstraints: [],
1200
+ checkConstraints: [],
1201
+ designerVersion: 2,
1056
1202
  dirty: false,
1057
1203
  schemaWarnings: [],
1058
1204
  fillImportedRows: false,
@@ -1151,6 +1297,32 @@ export function updateTableDesignerColumnField(draft, columnId, field, value, co
1151
1297
  );
1152
1298
  }
1153
1299
 
1300
+ export function updateTableDesignerConstraintField(
1301
+ draft,
1302
+ constraintKind,
1303
+ constraintId,
1304
+ field,
1305
+ value,
1306
+ context = {}
1307
+ ) {
1308
+ const collectionKey = constraintKind === "check" ? "checkConstraints" : "uniqueConstraints";
1309
+
1310
+ return recalculateTableDesignerDraft(
1311
+ {
1312
+ ...draft,
1313
+ [collectionKey]: (draft[collectionKey] ?? []).map((constraint) =>
1314
+ constraint.id === constraintId
1315
+ ? {
1316
+ ...constraint,
1317
+ [field]: value,
1318
+ }
1319
+ : constraint
1320
+ ),
1321
+ },
1322
+ context
1323
+ );
1324
+ }
1325
+
1154
1326
  export function addTableDesignerColumn(draft, context = {}) {
1155
1327
  return recalculateTableDesignerDraft(
1156
1328
  {
@@ -0,0 +1,20 @@
1
+ const CHARACTER_COUNT_FORMATTER = new Intl.NumberFormat("en-US");
2
+
3
+ export function getTextCellCharacterCount(value) {
4
+ if (typeof value !== "string" || value.length === 0) {
5
+ return null;
6
+ }
7
+
8
+ return Array.from(value).length;
9
+ }
10
+
11
+ export function formatTextCellCharacterCount(count) {
12
+ const numericCount = Number(count);
13
+
14
+ if (!Number.isFinite(numericCount) || numericCount < 0) {
15
+ return "";
16
+ }
17
+
18
+ const label = numericCount === 1 ? "char" : "chars";
19
+ return `${CHARACTER_COUNT_FORMATTER.format(numericCount)} ${label}`;
20
+ }
@@ -0,0 +1,264 @@
1
+ const MIN_TIMESTAMP_YEAR = 1990;
2
+ const MAX_TIMESTAMP_YEAR = 2100;
3
+ const MIN_TIMESTAMP_MS = Date.UTC(MIN_TIMESTAMP_YEAR, 0, 1, 0, 0, 0);
4
+ const MAX_TIMESTAMP_MS = Date.UTC(MAX_TIMESTAMP_YEAR, 11, 31, 23, 59, 59, 999);
5
+ const MIN_UNIX_SECONDS = Math.floor(MIN_TIMESTAMP_MS / 1000);
6
+ const MAX_UNIX_SECONDS = Math.floor(MAX_TIMESTAMP_MS / 1000);
7
+ const MIN_UNIX_MILLISECONDS = MIN_TIMESTAMP_MS;
8
+ const MAX_UNIX_MILLISECONDS = MAX_TIMESTAMP_MS;
9
+ const MIN_UNIX_MICROSECONDS = MIN_TIMESTAMP_MS * 1000;
10
+ const MAX_UNIX_MICROSECONDS = MAX_TIMESTAMP_MS * 1000;
11
+
12
+ function normalizeColumnName(value) {
13
+ return String(value ?? "").trim().toLowerCase();
14
+ }
15
+
16
+ function getColumnMeta(columnName, tableMeta = {}) {
17
+ const normalizedColumnName = normalizeColumnName(columnName);
18
+
19
+ return (tableMeta.columns ?? tableMeta.columnMeta ?? []).find(
20
+ (column) => normalizeColumnName(column?.name) === normalizedColumnName
21
+ );
22
+ }
23
+
24
+ function getForeignKeyColumnNames(tableMeta = {}) {
25
+ const names = new Set();
26
+
27
+ for (const foreignKey of tableMeta.foreignKeys ?? []) {
28
+ for (const mapping of foreignKey?.mappings ?? []) {
29
+ const from = normalizeColumnName(mapping?.from);
30
+
31
+ if (from) {
32
+ names.add(from);
33
+ }
34
+ }
35
+ }
36
+
37
+ return names;
38
+ }
39
+
40
+ export function isProtectedKeyColumn(columnName, tableMeta = {}) {
41
+ const normalizedColumnName = normalizeColumnName(columnName);
42
+
43
+ if (!normalizedColumnName) {
44
+ return false;
45
+ }
46
+
47
+ const column = getColumnMeta(columnName, tableMeta);
48
+
49
+ if (Number(column?.primaryKeyPosition ?? 0) > 0 || column?.primaryKey === true) {
50
+ return true;
51
+ }
52
+
53
+ if (column?.foreignKey === true) {
54
+ return true;
55
+ }
56
+
57
+ return getForeignKeyColumnNames(tableMeta).has(normalizedColumnName);
58
+ }
59
+
60
+ export function isIdLikeColumnName(columnName) {
61
+ const normalizedColumnName = normalizeColumnName(columnName);
62
+
63
+ if (!normalizedColumnName) {
64
+ return false;
65
+ }
66
+
67
+ return (
68
+ normalizedColumnName === "id" ||
69
+ normalizedColumnName === "rowid" ||
70
+ normalizedColumnName === "_id" ||
71
+ normalizedColumnName.endsWith("_id") ||
72
+ normalizedColumnName.endsWith(" id") ||
73
+ normalizedColumnName.includes("uuid") ||
74
+ normalizedColumnName.endsWith("_uuid") ||
75
+ normalizedColumnName.endsWith("_key")
76
+ );
77
+ }
78
+
79
+ function isPlausibleTimestampDate(date) {
80
+ if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
81
+ return false;
82
+ }
83
+
84
+ const time = date.getTime();
85
+ const year = date.getFullYear();
86
+
87
+ return (
88
+ time >= MIN_TIMESTAMP_MS &&
89
+ time <= MAX_TIMESTAMP_MS &&
90
+ year >= MIN_TIMESTAMP_YEAR &&
91
+ year <= MAX_TIMESTAMP_YEAR
92
+ );
93
+ }
94
+
95
+ function normalizeNumericTimestamp(value) {
96
+ const text = String(value ?? "").trim();
97
+
98
+ if (!/^\d+$/.test(text)) {
99
+ return null;
100
+ }
101
+
102
+ const numericValue = Number(text);
103
+
104
+ if (!Number.isSafeInteger(numericValue)) {
105
+ return null;
106
+ }
107
+
108
+ if (numericValue >= MIN_UNIX_MICROSECONDS && numericValue <= MAX_UNIX_MICROSECONDS) {
109
+ return {
110
+ date: new Date(Math.floor(numericValue / 1000)),
111
+ sourceFormat: "unix-microseconds",
112
+ };
113
+ }
114
+
115
+ if (numericValue >= MIN_UNIX_MILLISECONDS && numericValue <= MAX_UNIX_MILLISECONDS) {
116
+ return {
117
+ date: new Date(numericValue),
118
+ sourceFormat: "unix-milliseconds",
119
+ };
120
+ }
121
+
122
+ if (numericValue >= MIN_UNIX_SECONDS && numericValue <= MAX_UNIX_SECONDS) {
123
+ return {
124
+ date: new Date(numericValue * 1000),
125
+ sourceFormat: "unix-seconds",
126
+ };
127
+ }
128
+
129
+ return null;
130
+ }
131
+
132
+ function normalizeSqliteDateTimeString(value) {
133
+ const text = String(value ?? "").trim();
134
+ const match = text.match(
135
+ /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?$/
136
+ );
137
+
138
+ if (!match) {
139
+ return null;
140
+ }
141
+
142
+ const [, year, month, day, hour, minute, second = "0", millisecond = "0"] = match;
143
+ const date = new Date(
144
+ Number(year),
145
+ Number(month) - 1,
146
+ Number(day),
147
+ Number(hour),
148
+ Number(minute),
149
+ Number(second),
150
+ Number(millisecond.padEnd(3, "0"))
151
+ );
152
+
153
+ return {
154
+ date,
155
+ sourceFormat: "sqlite-datetime",
156
+ };
157
+ }
158
+
159
+ function normalizeIsoDateTimeString(value) {
160
+ const text = String(value ?? "").trim();
161
+
162
+ if (!/^\d{4}-\d{2}-\d{2}T/.test(text)) {
163
+ return null;
164
+ }
165
+
166
+ const date = new Date(text);
167
+
168
+ return {
169
+ date,
170
+ sourceFormat: "iso-datetime",
171
+ };
172
+ }
173
+
174
+ export function parseTimestampValue(value) {
175
+ if (value === null || value === undefined || value === "") {
176
+ return null;
177
+ }
178
+
179
+ const numericTimestamp = normalizeNumericTimestamp(value);
180
+
181
+ if (numericTimestamp && isPlausibleTimestampDate(numericTimestamp.date)) {
182
+ return numericTimestamp;
183
+ }
184
+
185
+ const isoTimestamp = normalizeIsoDateTimeString(value);
186
+
187
+ if (isoTimestamp && isPlausibleTimestampDate(isoTimestamp.date)) {
188
+ return isoTimestamp;
189
+ }
190
+
191
+ const sqliteTimestamp = normalizeSqliteDateTimeString(value);
192
+
193
+ if (sqliteTimestamp && isPlausibleTimestampDate(sqliteTimestamp.date)) {
194
+ return sqliteTimestamp;
195
+ }
196
+
197
+ return null;
198
+ }
199
+
200
+ function padDatePart(value) {
201
+ return String(value).padStart(2, "0");
202
+ }
203
+
204
+ export function formatTimestampPreview(date) {
205
+ if (!isPlausibleTimestampDate(date)) {
206
+ return "";
207
+ }
208
+
209
+ return [
210
+ padDatePart(date.getDate()),
211
+ ".",
212
+ padDatePart(date.getMonth() + 1),
213
+ ".",
214
+ date.getFullYear(),
215
+ ", ",
216
+ padDatePart(date.getHours()),
217
+ ":",
218
+ padDatePart(date.getMinutes()),
219
+ ":",
220
+ padDatePart(date.getSeconds()),
221
+ ].join("");
222
+ }
223
+
224
+ export function getTimestampPreviewForField({ columnName, value, tableMeta = {} } = {}) {
225
+ if (isProtectedKeyColumn(columnName, tableMeta)) {
226
+ return {
227
+ kind: "protected-key",
228
+ protected: true,
229
+ date: null,
230
+ formatted: "",
231
+ sourceFormat: "",
232
+ };
233
+ }
234
+
235
+ if (isIdLikeColumnName(columnName)) {
236
+ return {
237
+ kind: "none",
238
+ protected: false,
239
+ date: null,
240
+ formatted: "",
241
+ sourceFormat: "",
242
+ };
243
+ }
244
+
245
+ const parsed = parseTimestampValue(value);
246
+
247
+ if (!parsed) {
248
+ return {
249
+ kind: "none",
250
+ protected: false,
251
+ date: null,
252
+ formatted: "",
253
+ sourceFormat: "",
254
+ };
255
+ }
256
+
257
+ return {
258
+ kind: "timestamp",
259
+ protected: false,
260
+ date: parsed.date,
261
+ formatted: formatTimestampPreview(parsed.date),
262
+ sourceFormat: parsed.sourceFormat,
263
+ };
264
+ }
@@ -284,6 +284,8 @@ function renderQueryResultsSection(state) {
284
284
  editable: false,
285
285
  sortColumn: null,
286
286
  sortDirection: null,
287
+ resultScope: 'charts',
288
+ sortAction: null,
287
289
  })}
288
290
  </div>
289
291
  `
@@ -458,7 +460,7 @@ export function renderChartsDetail(state) {
458
460
  type="button"
459
461
  >
460
462
  <span class="material-symbols-outlined text-sm">${
461
- historyVisible ? 'visibility_off' : 'history'
463
+ historyVisible ? 'visibility_off' : 'visibility'
462
464
  }</span>
463
465
  ${historyVisible ? 'Hide Query History' : 'Show Query History'}
464
466
  </button>
@@ -1,6 +1,7 @@
1
1
  import { renderDataGrid } from '../components/dataGrid.js';
2
2
  import { renderRowEditorPanel } from '../components/rowEditorPanel.js';
3
3
  import { escapeHtml, formatCellValue, formatNumber, isBlobPreview, truncateMiddle } from '../utils/format.js';
4
+ import { compactPathForDisplay, detectFilePathValue } from '../utils/filePathPreview.js';
4
5
 
5
6
  function getSelectedRow(state) {
6
7
  if (state.dataBrowser.selectedRow) {
@@ -138,7 +139,11 @@ function renderWorkspaceHeader(state) {
138
139
  data-action="toggle-data-tables"
139
140
  type="button"
140
141
  >
141
- ${tablesVisible ? '<span class="material-symbols-outlined text-sm">visibility_off</span> Hide Tables' : 'Show Tables'}
142
+ ${
143
+ tablesVisible
144
+ ? '<span class="material-symbols-outlined text-sm">visibility_off</span> Hide Tables'
145
+ : '<span class="material-symbols-outlined text-sm">visibility</span> Show Tables'
146
+ }
142
147
  </button>
143
148
  <button
144
149
  class="standard-button"
@@ -362,15 +367,36 @@ function renderTableSurface(state) {
362
367
  }));
363
368
  const sortColumn = state.dataBrowser.sortColumn;
364
369
  const sortDirection = state.dataBrowser.sortDirection;
370
+ const tableMeta = {
371
+ columns: table.columnMeta ?? [],
372
+ foreignKeys: table.foreignKeys ?? [],
373
+ };
365
374
  const columns = (table.columns ?? []).map(columnName => ({
366
375
  headerClassName:
367
376
  'border-b border-primary-container/20 px-4 py-3 text-[10px] font-bold tracking-[0.08em] text-primary-container',
368
377
  renderHeader: () => renderSortableHeader(columnName, sortColumn, sortDirection, 'sort-data-column'),
369
378
  cellClassName: 'px-4 py-2 align-top text-[11px] text-on-surface',
370
379
  render: row => {
371
- const value = formatCellValue(row[columnName]);
380
+ const rawValue = row[columnName];
381
+ const filePath = detectFilePathValue(rawValue, columnName, tableMeta);
382
+ const value = formatCellValue(rawValue);
372
383
  const isNull = value === 'NULL';
373
384
  const widthClass = getCellWidthClass(columnName);
385
+
386
+ if (filePath) {
387
+ return `
388
+ <span
389
+ class="inline-flex ${widthClass} items-center gap-2 overflow-hidden whitespace-nowrap text-on-surface"
390
+ title="${escapeHtml(filePath.rawValue)}"
391
+ >
392
+ <span class="material-symbols-outlined text-sm text-on-surface-variant/55">folder</span>
393
+ <span class="min-w-0 overflow-hidden text-ellipsis">${escapeHtml(
394
+ compactPathForDisplay(filePath.rawValue, 48),
395
+ )}</span>
396
+ </span>
397
+ `;
398
+ }
399
+
374
400
  const displayValue = isNull ? value : truncateMiddle(value, 48);
375
401
 
376
402
  return `<span class="block ${widthClass} overflow-hidden text-ellipsis whitespace-nowrap ${
@@ -586,8 +612,13 @@ export function renderDataRowEditorPanel(state) {
586
612
  label: column.name,
587
613
  badges: getColumnBadges(column),
588
614
  },
615
+ rawValue: row[column.name],
589
616
  value: formatCellValue(row[column.name]),
590
617
  })),
618
+ tableMeta: {
619
+ columns: table.columnMeta ?? [],
620
+ foreignKeys: table.foreignKeys ?? [],
621
+ },
591
622
  saveError: state.dataBrowser.saveError,
592
623
  saving: state.dataBrowser.saving,
593
624
  deleting: state.dataBrowser.deleting,
@@ -595,6 +626,7 @@ export function renderDataRowEditorPanel(state) {
595
626
  deleteRowIndex: isIndexedRow ? rowIndex : null,
596
627
  deleteEnabled: Boolean(row.__identity),
597
628
  reloadAction: 'reload-data-route',
629
+ jsonActionsEnabled: true,
598
630
  });
599
631
  }
600
632