@walkthru-earth/objex 1.0.0 → 1.2.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 +11 -2
- package/dist/components/browser/FileBrowser.svelte +41 -54
- package/dist/components/browser/FileTreeSidebar.svelte +43 -7
- package/dist/components/layout/ConnectionDialog.svelte +100 -1
- package/dist/components/layout/Sidebar.svelte +43 -25
- package/dist/components/viewers/CodeViewer.svelte +23 -0
- package/dist/components/viewers/CogControls.svelte +208 -0
- package/dist/components/viewers/CogControls.svelte.d.ts +12 -0
- package/dist/components/viewers/CogViewer.svelte +353 -1160
- package/dist/components/viewers/CogViewer.svelte.d.ts +1 -1
- package/dist/components/viewers/DatabaseViewer.svelte +345 -37
- package/dist/components/viewers/MarkdownViewer.svelte +1 -1
- package/dist/components/viewers/TableViewer.svelte +123 -41
- package/dist/components/viewers/ZarrMapViewer.svelte +29 -0
- package/dist/components/viewers/ZarrViewer.svelte +1 -4
- package/dist/constants.d.ts +6 -2
- package/dist/constants.js +6 -2
- package/dist/file-icons/index.d.ts +1 -1
- package/dist/file-icons/index.js +12 -2
- package/dist/i18n/ar.js +24 -0
- package/dist/i18n/en.js +24 -0
- package/dist/i18n/index.svelte.d.ts +0 -1
- package/dist/i18n/index.svelte.js +0 -3
- package/dist/index.d.ts +11 -0
- package/dist/index.js +10 -0
- package/dist/query/engine.d.ts +20 -4
- package/dist/query/index.d.ts +2 -1
- package/dist/query/index.js +1 -0
- package/dist/query/source.d.ts +30 -0
- package/dist/query/source.js +37 -0
- package/dist/query/wasm.d.ts +7 -5
- package/dist/query/wasm.js +138 -85
- package/dist/storage/providers.d.ts +47 -0
- package/dist/storage/providers.js +160 -0
- package/dist/stores/connections.svelte.js +5 -31
- package/dist/stores/files.svelte.d.ts +2 -8
- package/dist/stores/files.svelte.js +5 -38
- package/dist/stores/query-history.svelte.js +3 -25
- package/dist/stores/settings.svelte.d.ts +1 -0
- package/dist/stores/settings.svelte.js +10 -30
- package/dist/stores/tabs.svelte.d.ts +9 -2
- package/dist/stores/tabs.svelte.js +11 -2
- package/dist/types.d.ts +11 -0
- package/dist/utils/cloud-url.d.ts +27 -0
- package/dist/utils/cloud-url.js +61 -0
- package/dist/utils/cog.d.ts +244 -0
- package/dist/utils/cog.js +1039 -0
- package/dist/utils/deck.d.ts +0 -18
- package/dist/utils/deck.js +0 -36
- package/dist/utils/export.d.ts +22 -2
- package/dist/utils/export.js +35 -10
- package/dist/utils/file-sort.d.ts +20 -0
- package/dist/utils/file-sort.js +41 -0
- package/dist/utils/geometry-type.d.ts +52 -0
- package/dist/utils/geometry-type.js +76 -0
- package/dist/utils/local-storage.d.ts +16 -0
- package/dist/utils/local-storage.js +37 -0
- package/dist/utils/markdown-sql.d.ts +1 -1
- package/dist/utils/markdown-sql.js +3 -4
- package/dist/utils/pmtiles-tile.d.ts +0 -2
- package/dist/utils/pmtiles-tile.js +0 -8
- package/dist/utils/url-state.d.ts +6 -0
- package/dist/utils/url-state.js +34 -26
- package/dist/utils/url.d.ts +13 -25
- package/dist/utils/url.js +17 -78
- package/dist/utils/zarr-tab.d.ts +22 -0
- package/dist/utils/zarr-tab.js +30 -0
- package/dist/utils/zarr.d.ts +0 -2
- package/dist/utils/zarr.js +73 -44
- package/package.json +50 -46
- package/dist/components/ui/tabs/index.d.ts +0 -5
- package/dist/components/ui/tabs/index.js +0 -7
- package/dist/components/ui/tabs/tabs-content.svelte +0 -17
- package/dist/components/ui/tabs/tabs-content.svelte.d.ts +0 -4
- package/dist/components/ui/tabs/tabs-list.svelte +0 -16
- package/dist/components/ui/tabs/tabs-list.svelte.d.ts +0 -4
- package/dist/components/ui/tabs/tabs-trigger.svelte +0 -20
- package/dist/components/ui/tabs/tabs-trigger.svelte.d.ts +0 -4
- package/dist/components/ui/tabs/tabs.svelte +0 -19
- package/dist/components/ui/tabs/tabs.svelte.d.ts +0 -4
- package/dist/components/viewers/MapViewer.svelte +0 -234
- package/dist/components/viewers/MapViewer.svelte.d.ts +0 -7
- package/dist/components/viewers/StyleEditorOverlay.svelte +0 -27
- package/dist/components/viewers/StyleEditorOverlay.svelte.d.ts +0 -7
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import ClockIcon from '@lucide/svelte/icons/clock';
|
|
2
3
|
import { onDestroy } from 'svelte';
|
|
3
4
|
import SqlEditor from '../editor/SqlEditor.svelte';
|
|
4
5
|
import { Badge } from '../ui/badge/index.js';
|
|
@@ -8,19 +9,40 @@ import { getQueryEngine } from '../../query/index.js';
|
|
|
8
9
|
import { getAdapter } from '../../storage/index.js';
|
|
9
10
|
import { tabResources } from '../../stores/tab-resources.svelte.js';
|
|
10
11
|
import type { Tab } from '../../types';
|
|
11
|
-
import
|
|
12
|
+
import TableViewer from './TableViewer.svelte';
|
|
12
13
|
|
|
13
14
|
let { tab }: { tab: Tab } = $props();
|
|
14
15
|
|
|
15
16
|
let loading = $state(true);
|
|
16
17
|
let error = $state<string | null>(null);
|
|
17
18
|
let tables = $state<string[]>([]);
|
|
19
|
+
let schemas = $state<string[]>([]);
|
|
20
|
+
let selectedSchema = $state<string>('main');
|
|
18
21
|
let selectedTable = $state<string | null>(null);
|
|
19
|
-
let columns = $state<string[]>([]);
|
|
20
|
-
let rows = $state<Record<string, any>[]>([]);
|
|
21
|
-
let tableLoading = $state(false);
|
|
22
22
|
let showSql = $state(false);
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Synthetic tab for the embedded TableViewer. Rebuilt whenever the user
|
|
26
|
+
* picks a different table, so TableViewer sees a new `tab.id` and runs its
|
|
27
|
+
* normal load lifecycle (including tabResources cleanup of the previous).
|
|
28
|
+
*/
|
|
29
|
+
let childTab = $state<Tab | null>(null);
|
|
30
|
+
|
|
31
|
+
// DuckLake state: true for .ducklake files, auto-detected for .duckdb files
|
|
32
|
+
let isDuckLake = $state(false);
|
|
33
|
+
let snapshots = $state.raw<Array<{ id: number; timeMs: number | null }>>([]);
|
|
34
|
+
let snapshotVersion = $state<number | null>(null);
|
|
35
|
+
let snapshotTimeMs = $state<number | null>(null);
|
|
36
|
+
let switchingSnapshot = $state(false);
|
|
37
|
+
|
|
38
|
+
const ATTACH_ALIAS = '__objex_db__';
|
|
39
|
+
|
|
40
|
+
// Virtual filesystem path for the downloaded catalog file
|
|
41
|
+
const VFS_PATH = `/${ATTACH_ALIAS}.duckdb`;
|
|
42
|
+
|
|
43
|
+
// DuckLake catalog tables used for auto-detection
|
|
44
|
+
const DUCKLAKE_MARKER_TABLES = ['ducklake_table', 'ducklake_schema', 'ducklake_snapshot'];
|
|
45
|
+
|
|
24
46
|
$effect(() => {
|
|
25
47
|
if (!tab) return;
|
|
26
48
|
loadDatabase();
|
|
@@ -28,9 +50,14 @@ $effect(() => {
|
|
|
28
50
|
|
|
29
51
|
function cleanup() {
|
|
30
52
|
tables = [];
|
|
31
|
-
|
|
32
|
-
columns = [];
|
|
53
|
+
schemas = [];
|
|
33
54
|
selectedTable = null;
|
|
55
|
+
childTab = null;
|
|
56
|
+
isDuckLake = false;
|
|
57
|
+
snapshots = [];
|
|
58
|
+
snapshotVersion = null;
|
|
59
|
+
snapshotTimeMs = null;
|
|
60
|
+
switchingSnapshot = false;
|
|
34
61
|
}
|
|
35
62
|
|
|
36
63
|
$effect(() => {
|
|
@@ -41,27 +68,56 @@ $effect(() => {
|
|
|
41
68
|
|
|
42
69
|
onDestroy(cleanup);
|
|
43
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Download the database file using the authenticated storage adapter
|
|
73
|
+
* and register it in DuckDB-WASM's virtual filesystem.
|
|
74
|
+
* DuckDB's ATTACH doesn't use httpfs S3 config, so we must download first.
|
|
75
|
+
*/
|
|
76
|
+
async function downloadAndRegister(engine: any): Promise<void> {
|
|
77
|
+
const adapter = getAdapter(tab.source, tab.connectionId);
|
|
78
|
+
const buffer = await adapter.read(tab.path);
|
|
79
|
+
if (engine.registerFileBuffer) {
|
|
80
|
+
// Drop any previously registered file with the same VFS path
|
|
81
|
+
if (engine.dropFile) {
|
|
82
|
+
await engine.dropFile(VFS_PATH);
|
|
83
|
+
}
|
|
84
|
+
await engine.registerFileBuffer(VFS_PATH, buffer);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
44
88
|
async function loadDatabase() {
|
|
45
89
|
loading = true;
|
|
46
90
|
error = null;
|
|
47
91
|
|
|
48
92
|
try {
|
|
49
|
-
// For DuckDB/SQLite files, we load them into DuckDB-WASM
|
|
50
93
|
const engine = await getQueryEngine();
|
|
51
94
|
const connId = tab.connectionId ?? '';
|
|
52
|
-
|
|
53
|
-
// Register the database file with DuckDB
|
|
54
|
-
// For .duckdb files, attach directly
|
|
55
|
-
// For .sqlite files, use sqlite scanner
|
|
56
95
|
const ext = tab.extension.toLowerCase();
|
|
57
96
|
|
|
58
|
-
if (ext === 'duckdb') {
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
97
|
+
if (ext === 'ducklake' || ext === 'duckdb') {
|
|
98
|
+
// Download and register in DuckDB-WASM's VFS for ATTACH
|
|
99
|
+
await downloadAndRegister(engine);
|
|
100
|
+
|
|
101
|
+
if (ext === 'ducklake') {
|
|
102
|
+
isDuckLake = true;
|
|
103
|
+
await loadDuckLake(engine, connId);
|
|
104
|
+
} else {
|
|
105
|
+
// .duckdb: auto-detect if it's a DuckLake catalog
|
|
106
|
+
const detected = await tryDetectDuckLake(engine, connId);
|
|
107
|
+
if (detected) {
|
|
108
|
+
isDuckLake = true;
|
|
109
|
+
await loadDuckLake(engine, connId);
|
|
110
|
+
} else {
|
|
111
|
+
isDuckLake = false;
|
|
112
|
+
const result = await engine.query(
|
|
113
|
+
connId,
|
|
114
|
+
`ATTACH '${VFS_PATH}' AS ${ATTACH_ALIAS} (READ_ONLY); SHOW TABLES;`
|
|
115
|
+
);
|
|
116
|
+
tables = (result.rows ?? [])
|
|
117
|
+
.map((row) => row.name)
|
|
118
|
+
.filter((name): name is string => !!name);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
65
121
|
} else {
|
|
66
122
|
// SQLite via DuckDB's sqlite scanner
|
|
67
123
|
const result = await engine.query(
|
|
@@ -77,22 +133,226 @@ async function loadDatabase() {
|
|
|
77
133
|
}
|
|
78
134
|
}
|
|
79
135
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
136
|
+
/**
|
|
137
|
+
* Probe a .duckdb file to check if it's a DuckLake catalog.
|
|
138
|
+
* Attaches as regular DuckDB, checks for DuckLake system tables,
|
|
139
|
+
* then detaches so loadDuckLake can re-attach with TYPE ducklake.
|
|
140
|
+
*/
|
|
141
|
+
async function tryDetectDuckLake(engine: any, connId: string): Promise<boolean> {
|
|
142
|
+
try {
|
|
143
|
+
await engine.query(connId, `ATTACH '${VFS_PATH}' AS ${ATTACH_ALIAS} (READ_ONLY);`);
|
|
144
|
+
const result = await engine.query(
|
|
145
|
+
connId,
|
|
146
|
+
`SELECT table_name FROM information_schema.tables WHERE table_catalog = '${ATTACH_ALIAS}' AND table_name IN ('${DUCKLAKE_MARKER_TABLES.join("','")}');`
|
|
147
|
+
);
|
|
148
|
+
// Detach before re-attaching as DuckLake
|
|
149
|
+
try {
|
|
150
|
+
await engine.query(connId, `DETACH ${ATTACH_ALIAS};`);
|
|
151
|
+
} catch {
|
|
152
|
+
// ignore
|
|
153
|
+
}
|
|
154
|
+
const found = (result.rows ?? []).map((r: any) => r.table_name);
|
|
155
|
+
// If at least 2 DuckLake system tables are present, it's a DuckLake catalog
|
|
156
|
+
return found.length >= 2;
|
|
157
|
+
} catch {
|
|
158
|
+
// If attach fails or query fails, not a DuckLake catalog
|
|
159
|
+
try {
|
|
160
|
+
await engine.query(connId, `DETACH ${ATTACH_ALIAS};`);
|
|
161
|
+
} catch {
|
|
162
|
+
// ignore
|
|
163
|
+
}
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
83
167
|
|
|
168
|
+
async function loadDuckLake(engine: any, connId: string, snapshotId: number | null = null) {
|
|
169
|
+
// Detach any previous catalog (ignore errors)
|
|
170
|
+
try {
|
|
171
|
+
await engine.query(connId, `DETACH ${ATTACH_ALIAS};`);
|
|
172
|
+
} catch {
|
|
173
|
+
// ignore
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// When snapshotId is given, attach at that snapshot for time travel; otherwise
|
|
177
|
+
// DuckLake attaches at the latest snapshot.
|
|
178
|
+
const snapshotClause = snapshotId !== null ? `, SNAPSHOT_VERSION ${snapshotId}` : '';
|
|
179
|
+
await engine.query(
|
|
180
|
+
connId,
|
|
181
|
+
`ATTACH '${VFS_PATH}' AS ${ATTACH_ALIAS} (TYPE ducklake, READ_ONLY${snapshotClause});`
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
// Snapshot history is immutable for a read-only attached catalog, so only
|
|
185
|
+
// query it on first load; switching snapshots reuses the cached list.
|
|
186
|
+
if (snapshots.length === 0) {
|
|
187
|
+
try {
|
|
188
|
+
const snapResult = await engine.query(
|
|
189
|
+
connId,
|
|
190
|
+
`SELECT snapshot_id, snapshot_time FROM ducklake_snapshots('${ATTACH_ALIAS}') ORDER BY snapshot_id DESC;`
|
|
191
|
+
);
|
|
192
|
+
const rows = snapResult.rows ?? [];
|
|
193
|
+
snapshots = rows.map((r: any) => ({
|
|
194
|
+
id: Number(r.snapshot_id),
|
|
195
|
+
timeMs: coerceTimestampMs(r.snapshot_time)
|
|
196
|
+
}));
|
|
197
|
+
} catch {
|
|
198
|
+
// Snapshot queries may fail on very old DuckLake specs; fall back silently.
|
|
199
|
+
snapshots = [];
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const current = snapshotId !== null ? snapshots.find((s) => s.id === snapshotId) : snapshots[0];
|
|
203
|
+
if (current) {
|
|
204
|
+
snapshotVersion = current.id;
|
|
205
|
+
snapshotTimeMs = current.timeMs;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Discover schemas
|
|
209
|
+
try {
|
|
210
|
+
const schemaResult = await engine.query(
|
|
211
|
+
connId,
|
|
212
|
+
`SELECT DISTINCT "schema" AS schema_name FROM (DESCRIBE) WHERE database = '${ATTACH_ALIAS}' ORDER BY "schema";`
|
|
213
|
+
);
|
|
214
|
+
schemas = (schemaResult.rows ?? [])
|
|
215
|
+
.map((r: any) => r.schema_name)
|
|
216
|
+
.filter((s: any): s is string => !!s);
|
|
217
|
+
if (schemas.length === 0) schemas = ['main'];
|
|
218
|
+
} catch {
|
|
219
|
+
schemas = ['main'];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Align selectedSchema with the discovered list. DuckLake catalogs often
|
|
223
|
+
// use 'default' or a user-named schema, not 'main', so the initial value
|
|
224
|
+
// would otherwise produce an empty table list until the user manually
|
|
225
|
+
// switches via the dropdown.
|
|
226
|
+
if (!schemas.includes(selectedSchema)) {
|
|
227
|
+
selectedSchema = schemas[0];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Load tables for selected schema
|
|
231
|
+
await loadDuckLakeTables(engine, connId);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function loadDuckLakeTables(engine: any, connId: string) {
|
|
235
|
+
const result = await engine.query(
|
|
236
|
+
connId,
|
|
237
|
+
`SELECT "name" AS table_name FROM (DESCRIBE) WHERE database = '${ATTACH_ALIAS}' AND "schema" = '${selectedSchema}' ORDER BY "name";`
|
|
238
|
+
);
|
|
239
|
+
tables = (result.rows ?? [])
|
|
240
|
+
.map((r: any) => r.table_name)
|
|
241
|
+
.filter((name: any): name is string => !!name);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function switchSnapshot(id: number) {
|
|
245
|
+
if (id === snapshotVersion) return;
|
|
246
|
+
switchingSnapshot = true;
|
|
247
|
+
error = null;
|
|
248
|
+
// Clear the currently-open table so TableViewer tears down and re-renders
|
|
249
|
+
// against the new catalog state.
|
|
250
|
+
selectedTable = null;
|
|
251
|
+
childTab = null;
|
|
84
252
|
try {
|
|
85
253
|
const engine = await getQueryEngine();
|
|
86
254
|
const connId = tab.connectionId ?? '';
|
|
87
|
-
|
|
88
|
-
columns = result.columns;
|
|
89
|
-
rows = result.rows ?? [];
|
|
255
|
+
await loadDuckLake(engine, connId, id);
|
|
90
256
|
} catch (err) {
|
|
91
257
|
error = err instanceof Error ? err.message : String(err);
|
|
92
258
|
} finally {
|
|
93
|
-
|
|
259
|
+
switchingSnapshot = false;
|
|
94
260
|
}
|
|
95
261
|
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* DuckDB-WASM returns TIMESTAMP in different shapes depending on the driver
|
|
265
|
+
* path: BigInt microseconds (Arrow int64), Number milliseconds, Date object,
|
|
266
|
+
* or ISO string. Normalize all of them to epoch milliseconds so downstream
|
|
267
|
+
* formatting has a single code path.
|
|
268
|
+
*/
|
|
269
|
+
function coerceTimestampMs(raw: unknown): number | null {
|
|
270
|
+
if (raw === null || raw === undefined) return null;
|
|
271
|
+
if (raw instanceof Date) {
|
|
272
|
+
const t = raw.getTime();
|
|
273
|
+
return Number.isNaN(t) ? null : t;
|
|
274
|
+
}
|
|
275
|
+
if (typeof raw === 'bigint') {
|
|
276
|
+
// DuckDB TIMESTAMP is microseconds since epoch.
|
|
277
|
+
return Number(raw / 1000n);
|
|
278
|
+
}
|
|
279
|
+
// Heuristic: > 1e14 is microseconds (any year past 5138 in ms is unrealistic)
|
|
280
|
+
const msFromEpochNumber = (n: number) => (n > 1e14 ? Math.floor(n / 1000) : n);
|
|
281
|
+
if (typeof raw === 'number') {
|
|
282
|
+
return Number.isFinite(raw) ? msFromEpochNumber(raw) : null;
|
|
283
|
+
}
|
|
284
|
+
if (typeof raw === 'string') {
|
|
285
|
+
const trimmed = raw.trim();
|
|
286
|
+
if (/^\d+$/.test(trimmed)) {
|
|
287
|
+
const n = Number(trimmed);
|
|
288
|
+
return Number.isFinite(n) ? msFromEpochNumber(n) : null;
|
|
289
|
+
}
|
|
290
|
+
const parsed = Date.parse(trimmed);
|
|
291
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
292
|
+
}
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function formatSnapshotTime(ms: number | null): string | null {
|
|
297
|
+
if (ms === null) return null;
|
|
298
|
+
const d = new Date(ms);
|
|
299
|
+
if (Number.isNaN(d.getTime())) return null;
|
|
300
|
+
return `${d
|
|
301
|
+
.toISOString()
|
|
302
|
+
.replace('T', ' ')
|
|
303
|
+
.replace(/\.\d+Z$/, '')} UTC`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function formatSnapshotLabel(s: { id: number; timeMs: number | null }): string {
|
|
307
|
+
const time = formatSnapshotTime(s.timeMs);
|
|
308
|
+
return time ? `v${s.id} (${time})` : `v${s.id}`;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function switchSchema(schema: string) {
|
|
312
|
+
selectedSchema = schema;
|
|
313
|
+
selectedTable = null;
|
|
314
|
+
childTab = null;
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
const engine = await getQueryEngine();
|
|
318
|
+
const connId = tab.connectionId ?? '';
|
|
319
|
+
await loadDuckLakeTables(engine, connId);
|
|
320
|
+
} catch (err) {
|
|
321
|
+
error = err instanceof Error ? err.message : String(err);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Build the FROM-clause target for a table inside the attached database.
|
|
327
|
+
* For DuckLake/DuckDB we fully-qualify via the attach alias and schema;
|
|
328
|
+
* for SQLite, the scanner exposes tables by bare name in the main catalog.
|
|
329
|
+
*/
|
|
330
|
+
function buildSourceRef(tableName: string): string {
|
|
331
|
+
// DuckLake / .duckdb tables live inside the attach alias; SQLite's scanner
|
|
332
|
+
// exposes tables by bare name in the main catalog.
|
|
333
|
+
const ext = tab.extension.toLowerCase();
|
|
334
|
+
if (isDuckLake || ext === 'duckdb') {
|
|
335
|
+
return `${ATTACH_ALIAS}."${selectedSchema}"."${tableName}"`;
|
|
336
|
+
}
|
|
337
|
+
return `"${tableName}"`;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function selectTable(tableName: string) {
|
|
341
|
+
selectedTable = tableName;
|
|
342
|
+
const ref = buildSourceRef(tableName);
|
|
343
|
+
// Synthetic tab: unique id per (db-tab, schema, table) so TableViewer
|
|
344
|
+
// re-runs its $effect-driven load and tabResources cleanup fires on
|
|
345
|
+
// the previous selection.
|
|
346
|
+
childTab = {
|
|
347
|
+
id: `${tab.id}::${selectedSchema}.${tableName}`,
|
|
348
|
+
name: `${selectedSchema}.${tableName}`,
|
|
349
|
+
path: `${tab.path}#${selectedSchema}.${tableName}`,
|
|
350
|
+
source: tab.source,
|
|
351
|
+
connectionId: tab.connectionId,
|
|
352
|
+
extension: 'parquet',
|
|
353
|
+
sourceRef: ref
|
|
354
|
+
};
|
|
355
|
+
}
|
|
96
356
|
</script>
|
|
97
357
|
|
|
98
358
|
<div class="flex h-full flex-col">
|
|
@@ -100,10 +360,36 @@ async function selectTable(tableName: string) {
|
|
|
100
360
|
class="flex items-center gap-1 border-b border-zinc-200 px-2 py-1.5 sm:gap-2 sm:px-4 dark:border-zinc-800"
|
|
101
361
|
>
|
|
102
362
|
<span class="truncate max-w-[120px] text-sm font-medium text-zinc-700 sm:max-w-none dark:text-zinc-300">{tab.name}</span>
|
|
103
|
-
|
|
363
|
+
{#if isDuckLake}
|
|
364
|
+
<Badge variant="secondary" class="bg-teal-100 text-teal-800 dark:bg-teal-900 dark:text-teal-200">DuckLake</Badge>
|
|
365
|
+
{:else}
|
|
366
|
+
<Badge variant="secondary">{t('database.badge')}</Badge>
|
|
367
|
+
{/if}
|
|
104
368
|
{#if tables.length > 0}
|
|
105
369
|
<span class="hidden text-xs text-zinc-400 sm:inline">{tables.length} {t('database.tables')}</span>
|
|
106
370
|
{/if}
|
|
371
|
+
{#if isDuckLake && snapshotVersion !== null}
|
|
372
|
+
<div class="hidden items-center gap-1 text-xs text-zinc-400 sm:inline-flex">
|
|
373
|
+
<ClockIcon class="h-3 w-3" />
|
|
374
|
+
{#if snapshots.length > 1}
|
|
375
|
+
<select
|
|
376
|
+
class="rounded bg-white px-1.5 py-0.5 text-xs text-zinc-700 disabled:opacity-60 dark:bg-zinc-800 dark:text-zinc-300"
|
|
377
|
+
disabled={switchingSnapshot}
|
|
378
|
+
title={t('ducklake.snapshot')}
|
|
379
|
+
value={snapshotVersion}
|
|
380
|
+
onchange={(e) => switchSnapshot(Number(e.currentTarget.value))}
|
|
381
|
+
>
|
|
382
|
+
{#each snapshots as snap (snap.id)}
|
|
383
|
+
<option value={snap.id}>{formatSnapshotLabel(snap)}</option>
|
|
384
|
+
{/each}
|
|
385
|
+
</select>
|
|
386
|
+
<span class="text-zinc-400">({snapshots.length} {t('ducklake.snapshots')})</span>
|
|
387
|
+
{:else}
|
|
388
|
+
{@const formatted = formatSnapshotTime(snapshotTimeMs)}
|
|
389
|
+
<span>v{snapshotVersion}{#if formatted} ({formatted}){/if}</span>
|
|
390
|
+
{/if}
|
|
391
|
+
</div>
|
|
392
|
+
{/if}
|
|
107
393
|
|
|
108
394
|
<div class="ms-auto">
|
|
109
395
|
<Button
|
|
@@ -120,17 +406,34 @@ async function selectTable(tableName: string) {
|
|
|
120
406
|
<div class="flex flex-1 overflow-hidden">
|
|
121
407
|
{#if loading}
|
|
122
408
|
<div class="flex flex-1 items-center justify-center">
|
|
123
|
-
<p class="text-sm text-zinc-400">{t('database.loading')}</p>
|
|
409
|
+
<p class="text-sm text-zinc-400">{isDuckLake ? t('ducklake.loading') : t('database.loading')}</p>
|
|
124
410
|
</div>
|
|
125
411
|
{:else if error}
|
|
126
|
-
<div class="flex flex-1 items-center justify-center">
|
|
127
|
-
<
|
|
412
|
+
<div class="flex flex-1 items-center justify-center p-4">
|
|
413
|
+
<div class="max-w-md text-center">
|
|
414
|
+
<p class="text-sm text-red-400">{error}</p>
|
|
415
|
+
{#if isDuckLake && error.includes('ducklake')}
|
|
416
|
+
<p class="mt-2 text-xs text-zinc-500">{t('ducklake.extensionHint')}</p>
|
|
417
|
+
{/if}
|
|
418
|
+
</div>
|
|
128
419
|
</div>
|
|
129
420
|
{:else}
|
|
130
|
-
<!-- Table list -->
|
|
421
|
+
<!-- Table list sidebar -->
|
|
131
422
|
<div
|
|
132
423
|
class="w-56 shrink-0 overflow-auto border-e border-zinc-200 bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-900"
|
|
133
424
|
>
|
|
425
|
+
{#if isDuckLake && schemas.length > 1}
|
|
426
|
+
<div class="border-b border-zinc-200 px-3 py-2 dark:border-zinc-800">
|
|
427
|
+
<select
|
|
428
|
+
class="w-full rounded bg-white px-2 py-1 text-xs text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300"
|
|
429
|
+
onchange={(e) => switchSchema(e.currentTarget.value)}
|
|
430
|
+
>
|
|
431
|
+
{#each schemas as schema}
|
|
432
|
+
<option value={schema} selected={schema === selectedSchema}>{schema}</option>
|
|
433
|
+
{/each}
|
|
434
|
+
</select>
|
|
435
|
+
</div>
|
|
436
|
+
{/if}
|
|
134
437
|
<div class="border-b border-zinc-200 px-3 py-2 dark:border-zinc-800">
|
|
135
438
|
<h3 class="text-xs font-medium text-zinc-500 dark:text-zinc-400">{t('database.tablesHeader')}</h3>
|
|
136
439
|
</div>
|
|
@@ -144,20 +447,25 @@ async function selectTable(tableName: string) {
|
|
|
144
447
|
<span class="text-zinc-700 dark:text-zinc-300">{tableName}</span>
|
|
145
448
|
</button>
|
|
146
449
|
{/each}
|
|
450
|
+
{#if tables.length === 0}
|
|
451
|
+
<div class="px-3 py-4 text-center text-xs text-zinc-400">
|
|
452
|
+
{isDuckLake ? t('ducklake.noTables') : t('database.selectTable')}
|
|
453
|
+
</div>
|
|
454
|
+
{/if}
|
|
147
455
|
</div>
|
|
148
456
|
|
|
149
|
-
<!-- Content
|
|
457
|
+
<!-- Content: embed TableViewer with a synthetic tab pointed at the
|
|
458
|
+
attached table. TableViewer handles SQL editing, CRS detection
|
|
459
|
+
from DuckDB v1.5 GEOMETRY types, and zero-copy WKB → map. -->
|
|
150
460
|
<div class="flex flex-1 flex-col overflow-hidden">
|
|
151
461
|
{#if showSql}
|
|
152
462
|
<div class="flex-1">
|
|
153
463
|
<SqlEditor connId={tab.connectionId ?? ''} />
|
|
154
464
|
</div>
|
|
155
|
-
{:else if
|
|
156
|
-
|
|
157
|
-
<
|
|
158
|
-
|
|
159
|
-
{:else if selectedTable && columns.length > 0}
|
|
160
|
-
<TableGrid {columns} {rows} />
|
|
465
|
+
{:else if childTab}
|
|
466
|
+
{#key childTab.id}
|
|
467
|
+
<TableViewer tab={childTab} />
|
|
468
|
+
{/key}
|
|
161
469
|
{:else}
|
|
162
470
|
<div class="flex flex-1 items-center justify-center">
|
|
163
471
|
<p class="text-sm text-zinc-400">{t('database.selectTable')}</p>
|
|
@@ -73,7 +73,7 @@ async function loadMarkdown() {
|
|
|
73
73
|
contentDir = isRTL ? 'rtl' : 'ltr';
|
|
74
74
|
|
|
75
75
|
// Parse for SQL blocks
|
|
76
|
-
const parsed = parseMarkdownDocument(rawMarkdown);
|
|
76
|
+
const parsed = await parseMarkdownDocument(rawMarkdown);
|
|
77
77
|
hasSqlBlocks = parsed.sqlBlocks.length > 0;
|
|
78
78
|
|
|
79
79
|
if (parsed.sqlBlocks.length > 0) {
|