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.
- package/README.md +52 -13
- package/database.sqlite +0 -0
- package/frontend/js/api.js +64 -12
- package/frontend/js/app.js +723 -37
- package/frontend/js/components/emptyState.js +42 -46
- package/frontend/js/components/modal.js +526 -2
- package/frontend/js/components/queryEditor.js +12 -3
- package/frontend/js/components/queryResults.js +79 -17
- package/frontend/js/components/rowEditorPanel.js +345 -12
- package/frontend/js/components/sidebar.js +106 -23
- package/frontend/js/components/tableDesignerEditor.js +69 -11
- package/frontend/js/store.js +437 -26
- package/frontend/js/utils/copyColumnExport.js +117 -0
- package/frontend/js/utils/exportFilenames.js +32 -0
- package/frontend/js/utils/filePathPreview.js +315 -0
- package/frontend/js/utils/format.js +1 -1
- package/frontend/js/utils/rowEditorJson.js +65 -0
- package/frontend/js/utils/sqlFormatter.js +691 -0
- package/frontend/js/utils/tableDesigner.js +178 -6
- package/frontend/js/utils/textCellStats.js +20 -0
- package/frontend/js/utils/timestampPreview.js +264 -0
- package/frontend/js/views/charts.js +3 -1
- package/frontend/js/views/data.js +39 -4
- package/frontend/js/views/editor.js +50 -1
- package/frontend/js/views/settings.js +1 -4
- package/frontend/js/views/structure.js +154 -212
- package/frontend/styles/base.css +6 -0
- package/frontend/styles/components.css +463 -2
- package/frontend/styles/structure-graph.css +0 -3
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/tokens.css +95 -95
- package/package.json +2 -3
- package/server/routes/export.js +97 -12
- package/server/services/sqlite/dataBrowserService.js +2 -68
- package/server/services/sqlite/exportService.js +74 -15
- package/server/services/sqlite/introspection.js +209 -1
- package/server/services/sqlite/sqlExecutor.js +31 -0
- package/server/services/sqlite/tableDesigner/changeAnalysis.js +25 -1
- package/server/services/sqlite/tableDesigner/schemaMapping.js +105 -10
- package/server/services/sqlite/tableDesigner/validation.js +60 -2
- package/server/services/sqlite/tableDesignerService.js +1 -1
- package/server/services/sqlite/tableFilter.js +75 -0
- package/server/utils/csv.js +30 -4
- package/tests/check-constraint-options.test.js +90 -0
- package/tests/export-filenames.test.js +34 -0
- package/tests/file-path-preview.test.js +165 -0
- package/tests/row-editor-json.test.js +82 -0
- package/tests/row-editor-timestamp-preview.test.js +192 -0
- package/tests/sql-formatter.test.js +173 -0
- package/tests/sql-highlight.test.js +38 -0
- package/tests/table-designer-v2-unique-constraints.test.js +78 -0
- package/tests/text-cell-stats.test.js +38 -0
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
784
|
+
return [
|
|
785
|
+
"-- No schema changes pending.",
|
|
786
|
+
...(
|
|
787
|
+
preservedConstraintComments.length
|
|
788
|
+
? ["", ...preservedConstraintComments]
|
|
789
|
+
: []
|
|
790
|
+
),
|
|
791
|
+
].join("\n");
|
|
660
792
|
}
|
|
661
793
|
|
|
662
|
-
return [
|
|
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' : '
|
|
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,14 +139,19 @@ function renderWorkspaceHeader(state) {
|
|
|
138
139
|
data-action="toggle-data-tables"
|
|
139
140
|
type="button"
|
|
140
141
|
>
|
|
141
|
-
${
|
|
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"
|
|
145
|
-
data-action="
|
|
150
|
+
data-action="open-data-export-modal"
|
|
146
151
|
type="button"
|
|
147
152
|
>
|
|
148
|
-
|
|
153
|
+
<span class="material-symbols-outlined text-sm">download</span>
|
|
154
|
+
${state.dataBrowser.exportLoading ? 'Exporting...' : 'Export'}
|
|
149
155
|
</button>`
|
|
150
156
|
: ''
|
|
151
157
|
}
|
|
@@ -361,15 +367,36 @@ function renderTableSurface(state) {
|
|
|
361
367
|
}));
|
|
362
368
|
const sortColumn = state.dataBrowser.sortColumn;
|
|
363
369
|
const sortDirection = state.dataBrowser.sortDirection;
|
|
370
|
+
const tableMeta = {
|
|
371
|
+
columns: table.columnMeta ?? [],
|
|
372
|
+
foreignKeys: table.foreignKeys ?? [],
|
|
373
|
+
};
|
|
364
374
|
const columns = (table.columns ?? []).map(columnName => ({
|
|
365
375
|
headerClassName:
|
|
366
376
|
'border-b border-primary-container/20 px-4 py-3 text-[10px] font-bold tracking-[0.08em] text-primary-container',
|
|
367
377
|
renderHeader: () => renderSortableHeader(columnName, sortColumn, sortDirection, 'sort-data-column'),
|
|
368
378
|
cellClassName: 'px-4 py-2 align-top text-[11px] text-on-surface',
|
|
369
379
|
render: row => {
|
|
370
|
-
const
|
|
380
|
+
const rawValue = row[columnName];
|
|
381
|
+
const filePath = detectFilePathValue(rawValue, columnName, tableMeta);
|
|
382
|
+
const value = formatCellValue(rawValue);
|
|
371
383
|
const isNull = value === 'NULL';
|
|
372
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
|
+
|
|
373
400
|
const displayValue = isNull ? value : truncateMiddle(value, 48);
|
|
374
401
|
|
|
375
402
|
return `<span class="block ${widthClass} overflow-hidden text-ellipsis whitespace-nowrap ${
|
|
@@ -574,6 +601,8 @@ export function renderDataRowEditorPanel(state) {
|
|
|
574
601
|
label: column.name,
|
|
575
602
|
badges: getColumnBadges(column),
|
|
576
603
|
...getColumnNumberInputMeta(column),
|
|
604
|
+
allowedValues: column.allowedValues ?? [],
|
|
605
|
+
notNull: Boolean(column.notNull),
|
|
577
606
|
value: value === null || value === undefined ? '' : String(value),
|
|
578
607
|
};
|
|
579
608
|
}),
|
|
@@ -583,8 +612,13 @@ export function renderDataRowEditorPanel(state) {
|
|
|
583
612
|
label: column.name,
|
|
584
613
|
badges: getColumnBadges(column),
|
|
585
614
|
},
|
|
615
|
+
rawValue: row[column.name],
|
|
586
616
|
value: formatCellValue(row[column.name]),
|
|
587
617
|
})),
|
|
618
|
+
tableMeta: {
|
|
619
|
+
columns: table.columnMeta ?? [],
|
|
620
|
+
foreignKeys: table.foreignKeys ?? [],
|
|
621
|
+
},
|
|
588
622
|
saveError: state.dataBrowser.saveError,
|
|
589
623
|
saving: state.dataBrowser.saving,
|
|
590
624
|
deleting: state.dataBrowser.deleting,
|
|
@@ -592,6 +626,7 @@ export function renderDataRowEditorPanel(state) {
|
|
|
592
626
|
deleteRowIndex: isIndexedRow ? rowIndex : null,
|
|
593
627
|
deleteEnabled: Boolean(row.__identity),
|
|
594
628
|
reloadAction: 'reload-data-route',
|
|
629
|
+
jsonActionsEnabled: true,
|
|
595
630
|
});
|
|
596
631
|
}
|
|
597
632
|
|