@walkthru-earth/objex 0.1.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/LICENSE +396 -0
- package/README.md +114 -0
- package/dist/assets/favicon.svg +17 -0
- package/dist/components/CLAUDE.md +44 -0
- package/dist/components/browser/Breadcrumb.svelte +50 -0
- package/dist/components/browser/Breadcrumb.svelte.d.ts +7 -0
- package/dist/components/browser/CreateFolderDialog.svelte +98 -0
- package/dist/components/browser/CreateFolderDialog.svelte.d.ts +6 -0
- package/dist/components/browser/DeleteConfirmDialog.svelte +90 -0
- package/dist/components/browser/DeleteConfirmDialog.svelte.d.ts +8 -0
- package/dist/components/browser/DropZone.svelte +83 -0
- package/dist/components/browser/DropZone.svelte.d.ts +7 -0
- package/dist/components/browser/FileBrowser.svelte +229 -0
- package/dist/components/browser/FileBrowser.svelte.d.ts +3 -0
- package/dist/components/browser/FileRow.svelte +112 -0
- package/dist/components/browser/FileRow.svelte.d.ts +9 -0
- package/dist/components/browser/FileTreeSidebar.svelte +559 -0
- package/dist/components/browser/FileTreeSidebar.svelte.d.ts +8 -0
- package/dist/components/browser/RenameDialog.svelte +101 -0
- package/dist/components/browser/RenameDialog.svelte.d.ts +8 -0
- package/dist/components/browser/SearchBar.svelte +40 -0
- package/dist/components/browser/SearchBar.svelte.d.ts +6 -0
- package/dist/components/browser/UploadButton.svelte +65 -0
- package/dist/components/browser/UploadButton.svelte.d.ts +3 -0
- package/dist/components/editor/CodeMirrorEditor.svelte +404 -0
- package/dist/components/editor/CodeMirrorEditor.svelte.d.ts +12 -0
- package/dist/components/editor/MilkdownEditor.svelte +98 -0
- package/dist/components/editor/MilkdownEditor.svelte.d.ts +9 -0
- package/dist/components/editor/SqlEditor.svelte +173 -0
- package/dist/components/editor/SqlEditor.svelte.d.ts +7 -0
- package/dist/components/editor/SqlResultBlock.svelte +199 -0
- package/dist/components/editor/SqlResultBlock.svelte.d.ts +9 -0
- package/dist/components/layout/ConnectionDialog.svelte +439 -0
- package/dist/components/layout/ConnectionDialog.svelte.d.ts +9 -0
- package/dist/components/layout/LocaleToggle.svelte +32 -0
- package/dist/components/layout/LocaleToggle.svelte.d.ts +3 -0
- package/dist/components/layout/SafeLockToggle.svelte +37 -0
- package/dist/components/layout/SafeLockToggle.svelte.d.ts +18 -0
- package/dist/components/layout/Sidebar.svelte +314 -0
- package/dist/components/layout/Sidebar.svelte.d.ts +3 -0
- package/dist/components/layout/StatusBar.svelte +73 -0
- package/dist/components/layout/StatusBar.svelte.d.ts +3 -0
- package/dist/components/layout/TabBar.svelte +102 -0
- package/dist/components/layout/TabBar.svelte.d.ts +7 -0
- package/dist/components/layout/ThemeToggle.svelte +52 -0
- package/dist/components/layout/ThemeToggle.svelte.d.ts +3 -0
- package/dist/components/ui/badge/badge.svelte +49 -0
- package/dist/components/ui/badge/badge.svelte.d.ts +32 -0
- package/dist/components/ui/badge/index.d.ts +1 -0
- package/dist/components/ui/badge/index.js +1 -0
- package/dist/components/ui/button/button.svelte +82 -0
- package/dist/components/ui/button/button.svelte.d.ts +64 -0
- package/dist/components/ui/button/index.d.ts +2 -0
- package/dist/components/ui/button/index.js +4 -0
- package/dist/components/ui/context-menu/context-menu-checkbox-item.svelte +40 -0
- package/dist/components/ui/context-menu/context-menu-checkbox-item.svelte.d.ts +9 -0
- package/dist/components/ui/context-menu/context-menu-content.svelte +28 -0
- package/dist/components/ui/context-menu/context-menu-content.svelte.d.ts +10 -0
- package/dist/components/ui/context-menu/context-menu-group-heading.svelte +21 -0
- package/dist/components/ui/context-menu/context-menu-group-heading.svelte.d.ts +7 -0
- package/dist/components/ui/context-menu/context-menu-group.svelte +7 -0
- package/dist/components/ui/context-menu/context-menu-group.svelte.d.ts +4 -0
- package/dist/components/ui/context-menu/context-menu-item.svelte +27 -0
- package/dist/components/ui/context-menu/context-menu-item.svelte.d.ts +8 -0
- package/dist/components/ui/context-menu/context-menu-label.svelte +24 -0
- package/dist/components/ui/context-menu/context-menu-label.svelte.d.ts +8 -0
- package/dist/components/ui/context-menu/context-menu-portal.svelte +7 -0
- package/dist/components/ui/context-menu/context-menu-portal.svelte.d.ts +3 -0
- package/dist/components/ui/context-menu/context-menu-radio-group.svelte +16 -0
- package/dist/components/ui/context-menu/context-menu-radio-group.svelte.d.ts +4 -0
- package/dist/components/ui/context-menu/context-menu-radio-item.svelte +33 -0
- package/dist/components/ui/context-menu/context-menu-radio-item.svelte.d.ts +4 -0
- package/dist/components/ui/context-menu/context-menu-separator.svelte +17 -0
- package/dist/components/ui/context-menu/context-menu-separator.svelte.d.ts +4 -0
- package/dist/components/ui/context-menu/context-menu-shortcut.svelte +20 -0
- package/dist/components/ui/context-menu/context-menu-shortcut.svelte.d.ts +5 -0
- package/dist/components/ui/context-menu/context-menu-sub-content.svelte +20 -0
- package/dist/components/ui/context-menu/context-menu-sub-content.svelte.d.ts +4 -0
- package/dist/components/ui/context-menu/context-menu-sub-trigger.svelte +29 -0
- package/dist/components/ui/context-menu/context-menu-sub-trigger.svelte.d.ts +8 -0
- package/dist/components/ui/context-menu/context-menu-sub.svelte +7 -0
- package/dist/components/ui/context-menu/context-menu-sub.svelte.d.ts +3 -0
- package/dist/components/ui/context-menu/context-menu-trigger.svelte +7 -0
- package/dist/components/ui/context-menu/context-menu-trigger.svelte.d.ts +4 -0
- package/dist/components/ui/context-menu/context-menu.svelte +7 -0
- package/dist/components/ui/context-menu/context-menu.svelte.d.ts +3 -0
- package/dist/components/ui/context-menu/index.d.ts +17 -0
- package/dist/components/ui/context-menu/index.js +19 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte +16 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte.d.ts +4 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +43 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte.d.ts +9 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-content.svelte +29 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-content.svelte.d.ts +10 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte +22 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte.d.ts +8 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-group.svelte +7 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-group.svelte.d.ts +4 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-item.svelte +27 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-item.svelte.d.ts +8 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-label.svelte +24 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-label.svelte.d.ts +8 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-portal.svelte +7 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-portal.svelte.d.ts +3 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte +16 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte.d.ts +4 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte +33 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte.d.ts +4 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-separator.svelte +17 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-separator.svelte.d.ts +4 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte +20 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte.d.ts +5 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte +20 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte.d.ts +4 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +29 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte.d.ts +7 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-sub.svelte +7 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-sub.svelte.d.ts +3 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +7 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-trigger.svelte.d.ts +4 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu.svelte +7 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu.svelte.d.ts +3 -0
- package/dist/components/ui/dropdown-menu/index.d.ts +18 -0
- package/dist/components/ui/dropdown-menu/index.js +18 -0
- package/dist/components/ui/input/index.d.ts +2 -0
- package/dist/components/ui/input/index.js +4 -0
- package/dist/components/ui/input/input.svelte +52 -0
- package/dist/components/ui/input/input.svelte.d.ts +13 -0
- package/dist/components/ui/resizable/index.d.ts +4 -0
- package/dist/components/ui/resizable/index.js +6 -0
- package/dist/components/ui/resizable/resizable-handle.svelte +30 -0
- package/dist/components/ui/resizable/resizable-handle.svelte.d.ts +8 -0
- package/dist/components/ui/resizable/resizable-pane-group.svelte +20 -0
- package/dist/components/ui/resizable/resizable-pane-group.svelte.d.ts +7 -0
- package/dist/components/ui/scroll-area/index.d.ts +3 -0
- package/dist/components/ui/scroll-area/index.js +5 -0
- package/dist/components/ui/scroll-area/scroll-area-scrollbar.svelte +31 -0
- package/dist/components/ui/scroll-area/scroll-area-scrollbar.svelte.d.ts +4 -0
- package/dist/components/ui/scroll-area/scroll-area.svelte +47 -0
- package/dist/components/ui/scroll-area/scroll-area.svelte.d.ts +11 -0
- package/dist/components/ui/separator/index.d.ts +2 -0
- package/dist/components/ui/separator/index.js +4 -0
- package/dist/components/ui/separator/separator.svelte +21 -0
- package/dist/components/ui/separator/separator.svelte.d.ts +4 -0
- package/dist/components/ui/sheet/index.d.ts +11 -0
- package/dist/components/ui/sheet/index.js +13 -0
- package/dist/components/ui/sheet/sheet-close.svelte +7 -0
- package/dist/components/ui/sheet/sheet-close.svelte.d.ts +4 -0
- package/dist/components/ui/sheet/sheet-content.svelte +62 -0
- package/dist/components/ui/sheet/sheet-content.svelte.d.ts +37 -0
- package/dist/components/ui/sheet/sheet-description.svelte +17 -0
- package/dist/components/ui/sheet/sheet-description.svelte.d.ts +4 -0
- package/dist/components/ui/sheet/sheet-footer.svelte +20 -0
- package/dist/components/ui/sheet/sheet-footer.svelte.d.ts +5 -0
- package/dist/components/ui/sheet/sheet-header.svelte +20 -0
- package/dist/components/ui/sheet/sheet-header.svelte.d.ts +5 -0
- package/dist/components/ui/sheet/sheet-overlay.svelte +20 -0
- package/dist/components/ui/sheet/sheet-overlay.svelte.d.ts +4 -0
- package/dist/components/ui/sheet/sheet-portal.svelte +7 -0
- package/dist/components/ui/sheet/sheet-portal.svelte.d.ts +3 -0
- package/dist/components/ui/sheet/sheet-title.svelte +13 -0
- package/dist/components/ui/sheet/sheet-title.svelte.d.ts +4 -0
- package/dist/components/ui/sheet/sheet-trigger.svelte +7 -0
- package/dist/components/ui/sheet/sheet-trigger.svelte.d.ts +4 -0
- package/dist/components/ui/sheet/sheet.svelte +7 -0
- package/dist/components/ui/sheet/sheet.svelte.d.ts +3 -0
- package/dist/components/ui/switch/index.d.ts +2 -0
- package/dist/components/ui/switch/index.js +4 -0
- package/dist/components/ui/switch/switch.svelte +28 -0
- package/dist/components/ui/switch/switch.svelte.d.ts +8 -0
- package/dist/components/ui/tabs/index.d.ts +5 -0
- package/dist/components/ui/tabs/index.js +7 -0
- package/dist/components/ui/tabs/tabs-content.svelte +17 -0
- package/dist/components/ui/tabs/tabs-content.svelte.d.ts +4 -0
- package/dist/components/ui/tabs/tabs-list.svelte +16 -0
- package/dist/components/ui/tabs/tabs-list.svelte.d.ts +4 -0
- package/dist/components/ui/tabs/tabs-trigger.svelte +20 -0
- package/dist/components/ui/tabs/tabs-trigger.svelte.d.ts +4 -0
- package/dist/components/ui/tabs/tabs.svelte +19 -0
- package/dist/components/ui/tabs/tabs.svelte.d.ts +4 -0
- package/dist/components/ui/tooltip/index.d.ts +6 -0
- package/dist/components/ui/tooltip/index.js +8 -0
- package/dist/components/ui/tooltip/tooltip-content.svelte +52 -0
- package/dist/components/ui/tooltip/tooltip-content.svelte.d.ts +11 -0
- package/dist/components/ui/tooltip/tooltip-portal.svelte +7 -0
- package/dist/components/ui/tooltip/tooltip-portal.svelte.d.ts +4 -0
- package/dist/components/ui/tooltip/tooltip-provider.svelte +7 -0
- package/dist/components/ui/tooltip/tooltip-provider.svelte.d.ts +4 -0
- package/dist/components/ui/tooltip/tooltip-trigger.svelte +7 -0
- package/dist/components/ui/tooltip/tooltip-trigger.svelte.d.ts +4 -0
- package/dist/components/ui/tooltip/tooltip.svelte +7 -0
- package/dist/components/ui/tooltip/tooltip.svelte.d.ts +4 -0
- package/dist/components/viewers/ArchiveViewer.svelte +586 -0
- package/dist/components/viewers/ArchiveViewer.svelte.d.ts +7 -0
- package/dist/components/viewers/CLAUDE.md +60 -0
- package/dist/components/viewers/CodeViewer.svelte +553 -0
- package/dist/components/viewers/CodeViewer.svelte.d.ts +7 -0
- package/dist/components/viewers/CogViewer.svelte +1345 -0
- package/dist/components/viewers/CogViewer.svelte.d.ts +7 -0
- package/dist/components/viewers/CopcViewer.svelte +25 -0
- package/dist/components/viewers/CopcViewer.svelte.d.ts +7 -0
- package/dist/components/viewers/DatabaseViewer.svelte +169 -0
- package/dist/components/viewers/DatabaseViewer.svelte.d.ts +7 -0
- package/dist/components/viewers/FileInfo.svelte +174 -0
- package/dist/components/viewers/FileInfo.svelte.d.ts +10 -0
- package/dist/components/viewers/FlatGeobufViewer.svelte +755 -0
- package/dist/components/viewers/FlatGeobufViewer.svelte.d.ts +7 -0
- package/dist/components/viewers/GeoParquetMapViewer.svelte +278 -0
- package/dist/components/viewers/GeoParquetMapViewer.svelte.d.ts +17 -0
- package/dist/components/viewers/ImageViewer.svelte +233 -0
- package/dist/components/viewers/ImageViewer.svelte.d.ts +7 -0
- package/dist/components/viewers/LoadProgress.svelte +93 -0
- package/dist/components/viewers/LoadProgress.svelte.d.ts +15 -0
- package/dist/components/viewers/MapViewer.svelte +234 -0
- package/dist/components/viewers/MapViewer.svelte.d.ts +7 -0
- package/dist/components/viewers/MarkdownViewer.svelte +478 -0
- package/dist/components/viewers/MarkdownViewer.svelte.d.ts +7 -0
- package/dist/components/viewers/MediaViewer.svelte +121 -0
- package/dist/components/viewers/MediaViewer.svelte.d.ts +7 -0
- package/dist/components/viewers/ModelViewer.svelte +164 -0
- package/dist/components/viewers/ModelViewer.svelte.d.ts +7 -0
- package/dist/components/viewers/NotebookViewer.svelte +389 -0
- package/dist/components/viewers/NotebookViewer.svelte.d.ts +7 -0
- package/dist/components/viewers/PdfViewer.svelte +278 -0
- package/dist/components/viewers/PdfViewer.svelte.d.ts +7 -0
- package/dist/components/viewers/PmtilesViewer.svelte +191 -0
- package/dist/components/viewers/PmtilesViewer.svelte.d.ts +7 -0
- package/dist/components/viewers/QueryHistoryPanel.svelte +159 -0
- package/dist/components/viewers/QueryHistoryPanel.svelte.d.ts +8 -0
- package/dist/components/viewers/RawViewer.svelte +117 -0
- package/dist/components/viewers/RawViewer.svelte.d.ts +7 -0
- package/dist/components/viewers/StacMapViewer.svelte +20 -0
- package/dist/components/viewers/StacMapViewer.svelte.d.ts +7 -0
- package/dist/components/viewers/StyleEditorOverlay.svelte +27 -0
- package/dist/components/viewers/StyleEditorOverlay.svelte.d.ts +7 -0
- package/dist/components/viewers/TableGrid.svelte +355 -0
- package/dist/components/viewers/TableGrid.svelte.d.ts +12 -0
- package/dist/components/viewers/TableStatusBar.svelte +92 -0
- package/dist/components/viewers/TableStatusBar.svelte.d.ts +11 -0
- package/dist/components/viewers/TableToolbar.svelte +382 -0
- package/dist/components/viewers/TableToolbar.svelte.d.ts +25 -0
- package/dist/components/viewers/TableViewer.svelte +923 -0
- package/dist/components/viewers/TableViewer.svelte.d.ts +7 -0
- package/dist/components/viewers/ViewerRouter.svelte +70 -0
- package/dist/components/viewers/ViewerRouter.svelte.d.ts +7 -0
- package/dist/components/viewers/ZarrMapViewer.svelte +288 -0
- package/dist/components/viewers/ZarrMapViewer.svelte.d.ts +17 -0
- package/dist/components/viewers/ZarrViewer.svelte +256 -0
- package/dist/components/viewers/ZarrViewer.svelte.d.ts +7 -0
- package/dist/components/viewers/map/AttributeTable.svelte +52 -0
- package/dist/components/viewers/map/AttributeTable.svelte.d.ts +8 -0
- package/dist/components/viewers/map/MapContainer.svelte +158 -0
- package/dist/components/viewers/map/MapContainer.svelte.d.ts +12 -0
- package/dist/components/viewers/pmtiles/PmtilesArchiveView.svelte +389 -0
- package/dist/components/viewers/pmtiles/PmtilesArchiveView.svelte.d.ts +10 -0
- package/dist/components/viewers/pmtiles/PmtilesMapView.svelte +332 -0
- package/dist/components/viewers/pmtiles/PmtilesMapView.svelte.d.ts +11 -0
- package/dist/components/viewers/pmtiles/PmtilesTileInspector.svelte +373 -0
- package/dist/components/viewers/pmtiles/PmtilesTileInspector.svelte.d.ts +12 -0
- package/dist/components/viewers/pmtiles/SvgTileRenderer.svelte +112 -0
- package/dist/components/viewers/pmtiles/SvgTileRenderer.svelte.d.ts +10 -0
- package/dist/file-icons/CLAUDE.md +21 -0
- package/dist/file-icons/FileTypeIcon.svelte +74 -0
- package/dist/file-icons/FileTypeIcon.svelte.d.ts +9 -0
- package/dist/file-icons/index.d.ts +56 -0
- package/dist/file-icons/index.js +1070 -0
- package/dist/i18n/CLAUDE.md +19 -0
- package/dist/i18n/ar.d.ts +1 -0
- package/dist/i18n/ar.js +404 -0
- package/dist/i18n/en.d.ts +1 -0
- package/dist/i18n/en.js +404 -0
- package/dist/i18n/index.svelte.d.ts +9 -0
- package/dist/i18n/index.svelte.js +27 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +13 -0
- package/dist/query/CLAUDE.md +22 -0
- package/dist/query/engine.d.ts +56 -0
- package/dist/query/engine.js +6 -0
- package/dist/query/index.d.ts +4 -0
- package/dist/query/index.js +19 -0
- package/dist/query/wasm.d.ts +20 -0
- package/dist/query/wasm.js +890 -0
- package/dist/storage/CLAUDE.md +23 -0
- package/dist/storage/adapter.d.ts +21 -0
- package/dist/storage/adapter.js +1 -0
- package/dist/storage/browser-azure.d.ts +25 -0
- package/dist/storage/browser-azure.js +271 -0
- package/dist/storage/browser-cloud.d.ts +32 -0
- package/dist/storage/browser-cloud.js +293 -0
- package/dist/storage/index.d.ts +11 -0
- package/dist/storage/index.js +37 -0
- package/dist/storage/url-adapter.d.ts +19 -0
- package/dist/storage/url-adapter.js +51 -0
- package/dist/stores/CLAUDE.md +29 -0
- package/dist/stores/browser.svelte.d.ts +28 -0
- package/dist/stores/browser.svelte.js +160 -0
- package/dist/stores/connections.svelte.d.ts +56 -0
- package/dist/stores/connections.svelte.js +272 -0
- package/dist/stores/credentials.svelte.d.ts +56 -0
- package/dist/stores/credentials.svelte.js +79 -0
- package/dist/stores/files.svelte.d.ts +20 -0
- package/dist/stores/files.svelte.js +76 -0
- package/dist/stores/query-history.svelte.d.ts +16 -0
- package/dist/stores/query-history.svelte.js +57 -0
- package/dist/stores/safelock.svelte.d.ts +8 -0
- package/dist/stores/safelock.svelte.js +52 -0
- package/dist/stores/settings.svelte.d.ts +11 -0
- package/dist/stores/settings.svelte.js +101 -0
- package/dist/stores/tab-resources.svelte.d.ts +25 -0
- package/dist/stores/tab-resources.svelte.js +61 -0
- package/dist/stores/tabs.svelte.d.ts +17 -0
- package/dist/stores/tabs.svelte.js +110 -0
- package/dist/types/notebookjs.d.ts +14 -0
- package/dist/types.d.ts +47 -0
- package/dist/types.js +1 -0
- package/dist/utils/CLAUDE.md +54 -0
- package/dist/utils/analytics.d.ts +10 -0
- package/dist/utils/analytics.js +38 -0
- package/dist/utils/archive.d.ts +70 -0
- package/dist/utils/archive.js +333 -0
- package/dist/utils/column-types.d.ts +5 -0
- package/dist/utils/column-types.js +137 -0
- package/dist/utils/deck.d.ts +98 -0
- package/dist/utils/deck.js +208 -0
- package/dist/utils/evidence-context.d.ts +22 -0
- package/dist/utils/evidence-context.js +56 -0
- package/dist/utils/export.d.ts +2 -0
- package/dist/utils/export.js +51 -0
- package/dist/utils/format.d.ts +14 -0
- package/dist/utils/format.js +56 -0
- package/dist/utils/geoarrow.d.ts +32 -0
- package/dist/utils/geoarrow.js +672 -0
- package/dist/utils/hex.d.ts +10 -0
- package/dist/utils/hex.js +27 -0
- package/dist/utils/host-detection.d.ts +23 -0
- package/dist/utils/host-detection.js +289 -0
- package/dist/utils/map-selection.d.ts +12 -0
- package/dist/utils/map-selection.js +45 -0
- package/dist/utils/markdown-sql.d.ts +30 -0
- package/dist/utils/markdown-sql.js +73 -0
- package/dist/utils/markdown.d.ts +18 -0
- package/dist/utils/markdown.js +146 -0
- package/dist/utils/model3d.d.ts +13 -0
- package/dist/utils/model3d.js +62 -0
- package/dist/utils/parquet-metadata.d.ts +58 -0
- package/dist/utils/parquet-metadata.js +228 -0
- package/dist/utils/pdf.d.ts +8 -0
- package/dist/utils/pdf.js +28 -0
- package/dist/utils/pmtiles-tile.d.ts +38 -0
- package/dist/utils/pmtiles-tile.js +64 -0
- package/dist/utils/pmtiles.d.ts +46 -0
- package/dist/utils/pmtiles.js +135 -0
- package/dist/utils/shiki.d.ts +8 -0
- package/dist/utils/shiki.js +98 -0
- package/dist/utils/storage-url.d.ts +64 -0
- package/dist/utils/storage-url.js +374 -0
- package/dist/utils/url-state.d.ts +40 -0
- package/dist/utils/url-state.js +113 -0
- package/dist/utils/url.d.ts +27 -0
- package/dist/utils/url.js +115 -0
- package/dist/utils/wkb.d.ts +43 -0
- package/dist/utils/wkb.js +345 -0
- package/dist/utils/zarr.d.ts +39 -0
- package/dist/utils/zarr.js +204 -0
- package/dist/utils.d.ts +12 -0
- package/dist/utils.js +5 -0
- package/package.json +203 -0
|
@@ -0,0 +1,890 @@
|
|
|
1
|
+
import { buildDuckDbSource } from '../file-icons/index.js';
|
|
2
|
+
import { credentialStore } from '../stores/credentials.svelte.js';
|
|
3
|
+
import { QueryCancelledError } from './engine';
|
|
4
|
+
const DUCKDB_VERSION = __DUCKDB_WASM_VERSION__;
|
|
5
|
+
const CDN_BASE = `https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm@${DUCKDB_VERSION}/dist`;
|
|
6
|
+
const duckdb_wasm = `${CDN_BASE}/duckdb-mvp.wasm`;
|
|
7
|
+
const mvp_worker = `${CDN_BASE}/duckdb-browser-mvp.worker.js`;
|
|
8
|
+
const duckdb_wasm_eh = `${CDN_BASE}/duckdb-eh.wasm`;
|
|
9
|
+
const eh_worker = `${CDN_BASE}/duckdb-browser-eh.worker.js`;
|
|
10
|
+
const INIT_TIMEOUT_MS = 30_000;
|
|
11
|
+
// ─── Performance & diagnostic logging ────────────────────────────────
|
|
12
|
+
const LOG_PREFIX = '[DuckDB]';
|
|
13
|
+
function log(...args) {
|
|
14
|
+
console.log(LOG_PREFIX, ...args);
|
|
15
|
+
}
|
|
16
|
+
function logWarn(...args) {
|
|
17
|
+
console.warn(LOG_PREFIX, ...args);
|
|
18
|
+
}
|
|
19
|
+
function elapsed(start) {
|
|
20
|
+
return `${(performance.now() - start).toFixed(1)}ms`;
|
|
21
|
+
}
|
|
22
|
+
let dbPromise = null;
|
|
23
|
+
function withTimeout(promise, ms, label) {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
26
|
+
promise.then((v) => {
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
resolve(v);
|
|
29
|
+
}, (e) => {
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
reject(e);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
async function getDB() {
|
|
36
|
+
if (dbPromise) {
|
|
37
|
+
log('getDB → cached');
|
|
38
|
+
return dbPromise;
|
|
39
|
+
}
|
|
40
|
+
dbPromise = (async () => {
|
|
41
|
+
const t0 = performance.now();
|
|
42
|
+
log('getDB → initializing...');
|
|
43
|
+
const duckdb = await import('@duckdb/duckdb-wasm');
|
|
44
|
+
const MANUAL_BUNDLES = {
|
|
45
|
+
mvp: {
|
|
46
|
+
mainModule: duckdb_wasm,
|
|
47
|
+
mainWorker: mvp_worker
|
|
48
|
+
},
|
|
49
|
+
eh: {
|
|
50
|
+
mainModule: duckdb_wasm_eh,
|
|
51
|
+
mainWorker: eh_worker
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const bundle = await duckdb.selectBundle(MANUAL_BUNDLES);
|
|
55
|
+
// Fetch the worker script as a blob to bypass cross-origin worker restrictions
|
|
56
|
+
const workerScript = await fetch(bundle.mainWorker).then((r) => r.blob());
|
|
57
|
+
const workerUrl = URL.createObjectURL(workerScript);
|
|
58
|
+
const worker = new Worker(workerUrl);
|
|
59
|
+
URL.revokeObjectURL(workerUrl);
|
|
60
|
+
const logger = new duckdb.ConsoleLogger();
|
|
61
|
+
const db = new duckdb.AsyncDuckDB(logger, worker);
|
|
62
|
+
await withTimeout(db.instantiate(bundle.mainModule, bundle.pthreadWorker), INIT_TIMEOUT_MS, 'DuckDB WASM instantiation');
|
|
63
|
+
log(`getDB → instantiated in ${elapsed(t0)}`);
|
|
64
|
+
// Load httpfs for remote file access and spatial for ST_ReadSHP
|
|
65
|
+
const conn = await db.connect();
|
|
66
|
+
try {
|
|
67
|
+
const tExt = performance.now();
|
|
68
|
+
await withTimeout(conn.query('INSTALL httpfs; LOAD httpfs; INSTALL spatial; LOAD spatial;'), INIT_TIMEOUT_MS, 'extension install (httpfs + spatial)');
|
|
69
|
+
// Disable auto-conversion of GeoParquet metadata → GEOMETRY type.
|
|
70
|
+
// Some files use legacy GeoParquet metadata (schema_version 0.x without
|
|
71
|
+
// "version" field) which causes DuckDB's spatial extension to throw
|
|
72
|
+
// "Geoparquet metadata does not have a version". We handle geometry
|
|
73
|
+
// detection, CRS, and WKB conversion ourselves via hyparquet metadata
|
|
74
|
+
// and explicit ST_GeomFromWKB() calls, so auto-conversion is not needed.
|
|
75
|
+
// SET GLOBAL applies to all future connections (no per-connection overhead).
|
|
76
|
+
try {
|
|
77
|
+
await conn.query('SET GLOBAL enable_geoparquet_conversion = false');
|
|
78
|
+
geoConversionGlobal = true;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// SET GLOBAL not supported — fall back to per-connection SET
|
|
82
|
+
await conn.query('SET enable_geoparquet_conversion = false');
|
|
83
|
+
}
|
|
84
|
+
log(`getDB → extensions loaded in ${elapsed(tExt)}`);
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
await conn.close();
|
|
88
|
+
}
|
|
89
|
+
log(`getDB → ready (total ${elapsed(t0)})`);
|
|
90
|
+
return db;
|
|
91
|
+
})();
|
|
92
|
+
// If init fails, clear the promise so it can be retried
|
|
93
|
+
dbPromise.catch(() => {
|
|
94
|
+
dbPromise = null;
|
|
95
|
+
});
|
|
96
|
+
return dbPromise;
|
|
97
|
+
}
|
|
98
|
+
let geoConversionGlobal = false;
|
|
99
|
+
/**
|
|
100
|
+
* Ensure GeoParquet auto-conversion is disabled on this connection.
|
|
101
|
+
* If SET GLOBAL succeeded during init, this is a no-op.
|
|
102
|
+
* Otherwise falls back to per-connection SET.
|
|
103
|
+
*/
|
|
104
|
+
async function ensureGeoConversionDisabled(conn) {
|
|
105
|
+
if (geoConversionGlobal)
|
|
106
|
+
return;
|
|
107
|
+
await conn.query('SET enable_geoparquet_conversion = false');
|
|
108
|
+
}
|
|
109
|
+
// ─── CRS detection helpers ───────────────────────────────────────────
|
|
110
|
+
const WGS84_CODES = new Set([4326, 4979]);
|
|
111
|
+
/** Extract EPSG code from a PROJJSON object. Returns null for WGS84/CRS84. */
|
|
112
|
+
function extractEpsgFromProjjson(crs) {
|
|
113
|
+
if (!crs)
|
|
114
|
+
return null;
|
|
115
|
+
// OGC CRS84 is lon/lat WGS84
|
|
116
|
+
if (crs.type === 'name' && crs.properties?.name?.includes('CRS84'))
|
|
117
|
+
return null;
|
|
118
|
+
// PROJJSON: { "id": { "authority": "EPSG", "code": 27700 } }
|
|
119
|
+
if (crs.id?.authority === 'EPSG') {
|
|
120
|
+
const code = crs.id.code;
|
|
121
|
+
if (WGS84_CODES.has(code))
|
|
122
|
+
return null;
|
|
123
|
+
return `EPSG:${code}`;
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Extract CRS from Parquet Format 2.11+ logical_type string.
|
|
129
|
+
* Handles these patterns:
|
|
130
|
+
* GeometryType(crs={...PROJJSON...}) — inline PROJJSON
|
|
131
|
+
* GeometryType(crs=projjson:key_name) — reference to KV metadata key
|
|
132
|
+
* GeometryType(crs=srid:5070) — direct SRID
|
|
133
|
+
* GeometryType(crs=<null>) — no CRS (WGS84)
|
|
134
|
+
*/
|
|
135
|
+
async function extractCrsFromLogicalType(logicalType, conn, path) {
|
|
136
|
+
if (!logicalType.startsWith('GeometryType(') && !logicalType.startsWith('GeographyType('))
|
|
137
|
+
return null;
|
|
138
|
+
// Extract the crs= value from inside the parentheses
|
|
139
|
+
const crsMatch = logicalType.match(/crs=(.+?)(?:,\s*\w+=|\))/s);
|
|
140
|
+
if (!crsMatch)
|
|
141
|
+
return null;
|
|
142
|
+
const crsValue = crsMatch[1].trim();
|
|
143
|
+
// Null CRS — assume WGS84
|
|
144
|
+
if (crsValue === '<null>' || crsValue === 'null')
|
|
145
|
+
return null;
|
|
146
|
+
// Pattern: srid:NNNN — direct SRID
|
|
147
|
+
const sridMatch = crsValue.match(/^srid:(\d+)$/);
|
|
148
|
+
if (sridMatch) {
|
|
149
|
+
const code = Number(sridMatch[1]);
|
|
150
|
+
if (WGS84_CODES.has(code))
|
|
151
|
+
return null;
|
|
152
|
+
return `EPSG:${code}`;
|
|
153
|
+
}
|
|
154
|
+
// Pattern: projjson:key_name — reference to a KV metadata key
|
|
155
|
+
const refMatch = crsValue.match(/^projjson:(.+)$/);
|
|
156
|
+
if (refMatch) {
|
|
157
|
+
try {
|
|
158
|
+
const kvResult = await conn.query(`SELECT value FROM parquet_kv_metadata('${path}') WHERE decode(key) = '${refMatch[1]}'`);
|
|
159
|
+
const kvRows = kvResult.toArray();
|
|
160
|
+
if (kvRows.length > 0) {
|
|
161
|
+
const raw = kvRows[0].value;
|
|
162
|
+
const text = typeof raw === 'string' ? raw : new TextDecoder().decode(raw);
|
|
163
|
+
return extractEpsgFromProjjson(JSON.parse(text));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
/* metadata lookup failed */
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
// Pattern: {... PROJJSON ...} — inline JSON (find balanced braces)
|
|
172
|
+
if (crsValue.startsWith('{')) {
|
|
173
|
+
const jsonStart = logicalType.indexOf('{');
|
|
174
|
+
if (jsonStart === -1)
|
|
175
|
+
return null;
|
|
176
|
+
let depth = 0;
|
|
177
|
+
let jsonEnd = -1;
|
|
178
|
+
for (let i = jsonStart; i < logicalType.length; i++) {
|
|
179
|
+
if (logicalType[i] === '{')
|
|
180
|
+
depth++;
|
|
181
|
+
else if (logicalType[i] === '}') {
|
|
182
|
+
depth--;
|
|
183
|
+
if (depth === 0) {
|
|
184
|
+
jsonEnd = i;
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (jsonEnd === -1)
|
|
190
|
+
return null;
|
|
191
|
+
try {
|
|
192
|
+
const crs = JSON.parse(logicalType.substring(jsonStart, jsonEnd + 1));
|
|
193
|
+
return extractEpsgFromProjjson(crs);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
// ─── Type-aware column extraction ────────────────────────────────────
|
|
202
|
+
// DuckDB Arrow type strings that represent binary/blob data — not useful
|
|
203
|
+
// for map tooltips and expensive to extract row-by-row.
|
|
204
|
+
const BINARY_TYPES = new Set(['BLOB', 'BYTEA', 'BINARY', 'LARGEBINARY', 'WKB_BLOB']);
|
|
205
|
+
/** True if the Arrow type string represents a numeric primitive (zero-copy .toArray()). */
|
|
206
|
+
function isNumericArrowType(typeStr) {
|
|
207
|
+
const t = typeStr.toUpperCase();
|
|
208
|
+
return (t.includes('INT') ||
|
|
209
|
+
t.includes('FLOAT') ||
|
|
210
|
+
t.includes('DOUBLE') ||
|
|
211
|
+
t.includes('DECIMAL') ||
|
|
212
|
+
t === 'TINYINT' ||
|
|
213
|
+
t === 'SMALLINT' ||
|
|
214
|
+
t === 'BIGINT' ||
|
|
215
|
+
t === 'HUGEINT' ||
|
|
216
|
+
t === 'UBIGINT' ||
|
|
217
|
+
t === 'UINTEGER' ||
|
|
218
|
+
t === 'USMALLINT' ||
|
|
219
|
+
t === 'UTINYINT');
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Extract column values using the fastest available method:
|
|
223
|
+
* - Numeric primitives → .toArray() returns a typed array view (zero-copy),
|
|
224
|
+
* then Array.from() to convert to a plain JS array for downstream compat.
|
|
225
|
+
* - Other types → per-element .get(i) for correctness (strings, structs, etc.)
|
|
226
|
+
*/
|
|
227
|
+
function extractColumnBulk(col, numRows, typeStr) {
|
|
228
|
+
if (isNumericArrowType(typeStr)) {
|
|
229
|
+
// .toArray() returns a TypedArray (Float64Array, Int32Array, etc.)
|
|
230
|
+
// which is a zero-copy view over the Arrow buffer.
|
|
231
|
+
return Array.from(col.toArray());
|
|
232
|
+
}
|
|
233
|
+
const values = new Array(numRows);
|
|
234
|
+
for (let i = 0; i < numRows; i++) {
|
|
235
|
+
values[i] = col.get(i);
|
|
236
|
+
}
|
|
237
|
+
return values;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Append column values from a streaming batch to an existing values array.
|
|
241
|
+
* Same optimisation as extractColumnBulk but appends instead of creating new.
|
|
242
|
+
*/
|
|
243
|
+
function appendColumnBulk(target, col, numRows, typeStr) {
|
|
244
|
+
if (isNumericArrowType(typeStr)) {
|
|
245
|
+
const arr = col.toArray();
|
|
246
|
+
for (let i = 0; i < arr.length; i++) {
|
|
247
|
+
target.push(arr[i]);
|
|
248
|
+
}
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
for (let i = 0; i < numRows; i++) {
|
|
252
|
+
target.push(col.get(i));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/** Should this column type be skipped for map attribute extraction? */
|
|
256
|
+
function isBinaryType(typeStr) {
|
|
257
|
+
return BINARY_TYPES.has(typeStr.toUpperCase());
|
|
258
|
+
}
|
|
259
|
+
export class WasmQueryEngine {
|
|
260
|
+
async query(connId, sql) {
|
|
261
|
+
const t0 = performance.now();
|
|
262
|
+
const sqlPreview = sql.length > 120 ? `${sql.slice(0, 120)}…` : sql;
|
|
263
|
+
log(`query → ${sqlPreview}`);
|
|
264
|
+
const db = await getDB();
|
|
265
|
+
const conn = await db.connect();
|
|
266
|
+
await ensureGeoConversionDisabled(conn);
|
|
267
|
+
const tConn = performance.now();
|
|
268
|
+
log(`query → connected in ${elapsed(t0)}`);
|
|
269
|
+
try {
|
|
270
|
+
if (connId) {
|
|
271
|
+
await this.configureStorage(conn, connId);
|
|
272
|
+
log(`query → storage configured in ${elapsed(tConn)}`);
|
|
273
|
+
}
|
|
274
|
+
const tQuery = performance.now();
|
|
275
|
+
const result = await conn.query(sql);
|
|
276
|
+
log(`query → executed in ${elapsed(tQuery)}, rows: ${result.numRows}`);
|
|
277
|
+
// DuckDB WASM returns an Arrow Table (bundled apache-arrow@17).
|
|
278
|
+
// Our project uses apache-arrow@21 — cross-version tableToIPC/tableFromIPC
|
|
279
|
+
// loses data rows. Extract rows directly from DuckDB's own Arrow Table.
|
|
280
|
+
const numRows = result.numRows;
|
|
281
|
+
const cols = result.schema.fields.map((f) => f.name);
|
|
282
|
+
const types = result.schema.fields.map((f) => String(f.type));
|
|
283
|
+
if (numRows === 0) {
|
|
284
|
+
log(`query → done (empty) in ${elapsed(t0)}`);
|
|
285
|
+
return {
|
|
286
|
+
columns: cols,
|
|
287
|
+
types,
|
|
288
|
+
rowCount: 0,
|
|
289
|
+
rows: []
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
// Extract rows directly — avoids Arrow version mismatch
|
|
293
|
+
const rows = result.toArray().map((row) => {
|
|
294
|
+
if (typeof row.toJSON === 'function')
|
|
295
|
+
return row.toJSON();
|
|
296
|
+
// Fallback: manually build row object
|
|
297
|
+
const obj = {};
|
|
298
|
+
for (const col of cols)
|
|
299
|
+
obj[col] = row[col];
|
|
300
|
+
return obj;
|
|
301
|
+
});
|
|
302
|
+
log(`query → done in ${elapsed(t0)}, ${numRows} rows, ${cols.length} cols`);
|
|
303
|
+
return { columns: cols, types, rowCount: numRows, rows };
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
logWarn(`query → failed after ${elapsed(t0)}:`, err?.message ?? err);
|
|
307
|
+
throw err;
|
|
308
|
+
}
|
|
309
|
+
finally {
|
|
310
|
+
await conn.close();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
async queryForMap(connId, sql, geomCol, geomColType, sourceCrs) {
|
|
314
|
+
const t0 = performance.now();
|
|
315
|
+
log(`queryForMap → geomCol: ${geomCol}, type: ${geomColType}, crs: ${sourceCrs ?? 'WGS84'}`);
|
|
316
|
+
const db = await getDB();
|
|
317
|
+
const conn = await db.connect();
|
|
318
|
+
await ensureGeoConversionDisabled(conn);
|
|
319
|
+
try {
|
|
320
|
+
if (connId) {
|
|
321
|
+
await this.configureStorage(conn, connId);
|
|
322
|
+
}
|
|
323
|
+
// Build geometry expression based on column type:
|
|
324
|
+
// - Native spatial types (GEOMETRY, WKB_BLOB, POINT, etc.) → use directly
|
|
325
|
+
// - BLOB/BINARY → DuckDB implicitly casts BLOB→GEOMETRY, use directly
|
|
326
|
+
// - Everything else (VARCHAR, JSON, STRUCT, ...) → GeoJSON text
|
|
327
|
+
const quoted = `"${geomCol}"`;
|
|
328
|
+
const upper = geomColType.toUpperCase();
|
|
329
|
+
// Spatial types that ST_AsWKB accepts directly (GEOMETRY, WKB_BLOB, etc.).
|
|
330
|
+
// Includes Arrow "Binary"/"LargeBinary" — DuckDB GEOMETRY columns from
|
|
331
|
+
// ST_ReadSHP/ST_Read appear as Arrow Binary but are NOT WKB blobs.
|
|
332
|
+
const isSpatialType = upper === 'GEOMETRY' ||
|
|
333
|
+
upper === 'GEOGRAPHY' ||
|
|
334
|
+
upper === 'WKB_BLOB' ||
|
|
335
|
+
upper.includes('POINT') ||
|
|
336
|
+
upper.includes('LINESTRING') ||
|
|
337
|
+
upper.includes('POLYGON') ||
|
|
338
|
+
upper.includes('BINARY'); // Arrow serialization of DuckDB GEOMETRY
|
|
339
|
+
// Actual WKB BLOB columns (e.g. GeoParquet) need explicit ST_GeomFromWKB
|
|
340
|
+
// because DuckDB has no implicit BLOB→GEOMETRY cast.
|
|
341
|
+
const isWkbBlob = upper === 'BLOB' || upper === 'BYTEA';
|
|
342
|
+
let wkbExpr;
|
|
343
|
+
let geomExpr;
|
|
344
|
+
if (isWkbBlob && !sourceCrs) {
|
|
345
|
+
// Already WKB — use directly, no spatial function calls needed.
|
|
346
|
+
// Avoids ST_GeomFromWKB which can fail if DuckDB auto-converted
|
|
347
|
+
// the column to GEOMETRY despite enable_geoparquet_conversion=false.
|
|
348
|
+
wkbExpr = quoted;
|
|
349
|
+
geomExpr = null; // geometry type detected client-side from WKB headers
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
geomExpr = isSpatialType
|
|
353
|
+
? quoted
|
|
354
|
+
: isWkbBlob
|
|
355
|
+
? `ST_GeomFromWKB(${quoted})`
|
|
356
|
+
: `ST_GeomFromGeoJSON(${quoted})`;
|
|
357
|
+
// Re-project to WGS84 if the source CRS is not EPSG:4326/CRS84.
|
|
358
|
+
// always_xy := true forces lon/lat (x/y) axis order for both source and
|
|
359
|
+
// target, matching the GeoParquet convention regardless of CRS authority.
|
|
360
|
+
if (sourceCrs) {
|
|
361
|
+
geomExpr = `ST_Transform(${geomExpr}, '${sourceCrs}', 'EPSG:4326', always_xy := true)`;
|
|
362
|
+
}
|
|
363
|
+
// ST_AsWKB needed — DuckDB GEOMETRY columns (from ST_ReadSHP, ST_Read)
|
|
364
|
+
// use an internal binary format, not WKB, even though Arrow reports Binary type.
|
|
365
|
+
wkbExpr = `ST_AsWKB(${geomExpr})`;
|
|
366
|
+
}
|
|
367
|
+
// Wrap query: WKB for binary geometry, optional ST_GeometryType
|
|
368
|
+
const mapSql = geomExpr
|
|
369
|
+
? `SELECT *, ${wkbExpr} AS __wkb, ST_GeometryType(${geomExpr}) AS __geom_type FROM (${sql}) __src`
|
|
370
|
+
: `SELECT *, ${wkbExpr} AS __wkb FROM (${sql}) __src`;
|
|
371
|
+
const result = await conn.query(mapSql);
|
|
372
|
+
// Extract raw WKB binary column — .get(i) returns Uint8Array from Arrow v17
|
|
373
|
+
const wkbCol = result.getChild('__wkb');
|
|
374
|
+
const wkbArrays = [];
|
|
375
|
+
for (let i = 0; i < wkbCol.length; i++) {
|
|
376
|
+
const v = wkbCol.get(i);
|
|
377
|
+
// Copy: Arrow .get() returns buffer views that may be invalidated
|
|
378
|
+
if (v)
|
|
379
|
+
wkbArrays.push(v instanceof Uint8Array ? v.slice() : new Uint8Array(v));
|
|
380
|
+
}
|
|
381
|
+
// Detect geometry type from first non-null row (if available)
|
|
382
|
+
const typeCol = result.getChild('__geom_type');
|
|
383
|
+
let geometryType = 'POINT';
|
|
384
|
+
if (typeCol) {
|
|
385
|
+
for (let i = 0; i < typeCol.length; i++) {
|
|
386
|
+
const t = typeCol.get(i);
|
|
387
|
+
if (t) {
|
|
388
|
+
geometryType = String(t);
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
// Extract attribute columns (skip geometry, helper, and binary columns)
|
|
394
|
+
const skipCols = new Set([geomCol, '__wkb', '__geom_type']);
|
|
395
|
+
const attributes = new Map();
|
|
396
|
+
for (const field of result.schema.fields) {
|
|
397
|
+
if (skipCols.has(field.name))
|
|
398
|
+
continue;
|
|
399
|
+
const typeStr = String(field.type);
|
|
400
|
+
// Skip binary/blob columns — not useful for map tooltips, expensive to extract
|
|
401
|
+
if (isBinaryType(typeStr))
|
|
402
|
+
continue;
|
|
403
|
+
const col = result.getChild(field.name);
|
|
404
|
+
const values = extractColumnBulk(col, col.length, typeStr);
|
|
405
|
+
attributes.set(field.name, { values, type: typeStr });
|
|
406
|
+
}
|
|
407
|
+
log(`queryForMap → done in ${elapsed(t0)}, ${wkbArrays.length} geometries (${geometryType}), ${attributes.size} attrs`);
|
|
408
|
+
return { wkbArrays, geometryType, attributes, rowCount: wkbArrays.length };
|
|
409
|
+
}
|
|
410
|
+
catch (err) {
|
|
411
|
+
logWarn(`queryForMap → failed after ${elapsed(t0)}:`, err?.message ?? err);
|
|
412
|
+
throw err;
|
|
413
|
+
}
|
|
414
|
+
finally {
|
|
415
|
+
await conn.close();
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
async getSchema(connId, path) {
|
|
419
|
+
const t0 = performance.now();
|
|
420
|
+
log('getSchema →', path);
|
|
421
|
+
const db = await getDB();
|
|
422
|
+
const conn = await db.connect();
|
|
423
|
+
await ensureGeoConversionDisabled(conn);
|
|
424
|
+
try {
|
|
425
|
+
if (connId) {
|
|
426
|
+
await this.configureStorage(conn, connId);
|
|
427
|
+
}
|
|
428
|
+
const source = buildDuckDbSource(path, path);
|
|
429
|
+
const result = await conn.query(`DESCRIBE SELECT * FROM ${source}`);
|
|
430
|
+
const rows = result.toArray();
|
|
431
|
+
const schema = rows.map((row) => ({
|
|
432
|
+
name: row.column_name,
|
|
433
|
+
type: row.column_type,
|
|
434
|
+
nullable: row.null === 'YES'
|
|
435
|
+
}));
|
|
436
|
+
log(`getSchema → done in ${elapsed(t0)}, ${schema.length} fields`);
|
|
437
|
+
return schema;
|
|
438
|
+
}
|
|
439
|
+
catch (err) {
|
|
440
|
+
logWarn('getSchema → failed:', err?.message ?? err);
|
|
441
|
+
throw err;
|
|
442
|
+
}
|
|
443
|
+
finally {
|
|
444
|
+
await conn.close();
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
async getRowCount(connId, path) {
|
|
448
|
+
const t0 = performance.now();
|
|
449
|
+
log('getRowCount →', path);
|
|
450
|
+
const db = await getDB();
|
|
451
|
+
const conn = await db.connect();
|
|
452
|
+
await ensureGeoConversionDisabled(conn);
|
|
453
|
+
try {
|
|
454
|
+
if (connId) {
|
|
455
|
+
await this.configureStorage(conn, connId);
|
|
456
|
+
}
|
|
457
|
+
// For Parquet files, try reading row count from file footer metadata first.
|
|
458
|
+
// This avoids parsing column types (which can fail on exotic geometry types)
|
|
459
|
+
// and is faster than SELECT COUNT(*) since it reads only footer bytes.
|
|
460
|
+
const isParquet = /\.parquet$/i.test(path);
|
|
461
|
+
if (isParquet) {
|
|
462
|
+
try {
|
|
463
|
+
const metaResult = await conn.query(`SELECT SUM(num_rows)::BIGINT as cnt FROM parquet_file_metadata('${path}')`);
|
|
464
|
+
const metaRows = metaResult.toArray();
|
|
465
|
+
const count = Number(metaRows[0].cnt);
|
|
466
|
+
log(`getRowCount → ${count} via parquet_file_metadata in ${elapsed(t0)}`);
|
|
467
|
+
return count;
|
|
468
|
+
}
|
|
469
|
+
catch (metaErr) {
|
|
470
|
+
logWarn('getRowCount → parquet_file_metadata failed, falling back to COUNT(*):', metaErr?.message ?? metaErr);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const source = buildDuckDbSource(path, path);
|
|
474
|
+
const result = await conn.query(`SELECT COUNT(*) as cnt FROM ${source}`);
|
|
475
|
+
const rows = result.toArray();
|
|
476
|
+
const count = Number(rows[0].cnt);
|
|
477
|
+
log(`getRowCount → ${count} via COUNT(*) in ${elapsed(t0)}`);
|
|
478
|
+
return count;
|
|
479
|
+
}
|
|
480
|
+
catch (err) {
|
|
481
|
+
logWarn('getRowCount → failed:', err?.message ?? err);
|
|
482
|
+
throw err;
|
|
483
|
+
}
|
|
484
|
+
finally {
|
|
485
|
+
await conn.close();
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
async getSchemaAndCrs(connId, path, findGeoCol) {
|
|
489
|
+
const t0 = performance.now();
|
|
490
|
+
log('getSchemaAndCrs →', path);
|
|
491
|
+
const db = await getDB();
|
|
492
|
+
const conn = await db.connect();
|
|
493
|
+
await ensureGeoConversionDisabled(conn);
|
|
494
|
+
try {
|
|
495
|
+
if (connId) {
|
|
496
|
+
await this.configureStorage(conn, connId);
|
|
497
|
+
}
|
|
498
|
+
// Schema detection
|
|
499
|
+
const tSchema = performance.now();
|
|
500
|
+
const source = buildDuckDbSource(path, path);
|
|
501
|
+
const result = await conn.query(`DESCRIBE SELECT * FROM ${source}`);
|
|
502
|
+
const schemaRows = result.toArray();
|
|
503
|
+
const schema = schemaRows.map((row) => ({
|
|
504
|
+
name: row.column_name,
|
|
505
|
+
type: row.column_type,
|
|
506
|
+
nullable: row.null === 'YES'
|
|
507
|
+
}));
|
|
508
|
+
log(`getSchemaAndCrs → schema: ${schema.length} fields in ${elapsed(tSchema)}`);
|
|
509
|
+
// Geo column detection via callback (avoids importing wkb utils here)
|
|
510
|
+
const geomCol = findGeoCol(schema);
|
|
511
|
+
if (!geomCol) {
|
|
512
|
+
log(`getSchemaAndCrs → no geo column, done in ${elapsed(t0)}`);
|
|
513
|
+
return { schema, geomCol: null, crs: null };
|
|
514
|
+
}
|
|
515
|
+
// CRS detection reusing the same connection
|
|
516
|
+
log(`getSchemaAndCrs → geo column: ${geomCol}, detecting CRS...`);
|
|
517
|
+
const tCrs = performance.now();
|
|
518
|
+
const crs = await this.detectCrsWithConn(conn, path, geomCol);
|
|
519
|
+
log(`getSchemaAndCrs → CRS: ${crs ?? 'WGS84/null'} in ${elapsed(tCrs)}, total ${elapsed(t0)}`);
|
|
520
|
+
return { schema, geomCol, crs };
|
|
521
|
+
}
|
|
522
|
+
catch (err) {
|
|
523
|
+
logWarn('getSchemaAndCrs → failed:', err?.message ?? err);
|
|
524
|
+
throw err;
|
|
525
|
+
}
|
|
526
|
+
finally {
|
|
527
|
+
await conn.close();
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
async configureStorage(conn, connId) {
|
|
531
|
+
try {
|
|
532
|
+
// Read connection metadata from localStorage
|
|
533
|
+
const stored = localStorage.getItem('obstore-explore-connections');
|
|
534
|
+
if (!stored) {
|
|
535
|
+
log('configureStorage → no connections in localStorage');
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const connections = JSON.parse(stored);
|
|
539
|
+
const connection = connections.find((c) => c.id === connId);
|
|
540
|
+
if (!connection) {
|
|
541
|
+
logWarn(`configureStorage → connection "${connId}" not found`);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
// Azure uses direct HTTPS URLs with SAS token — no S3 config needed
|
|
545
|
+
if (connection.provider === 'azure') {
|
|
546
|
+
log('configureStorage → Azure provider, skipping S3 config');
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
// Batch all SET commands into a single query to minimize web worker round-trips
|
|
550
|
+
const sets = [];
|
|
551
|
+
// Set S3 credentials from in-memory credential store
|
|
552
|
+
const creds = credentialStore.get(connId);
|
|
553
|
+
if (creds && creds.type === 'sigv4') {
|
|
554
|
+
sets.push(`SET s3_access_key_id = '${creds.accessKey}'`);
|
|
555
|
+
sets.push(`SET s3_secret_access_key = '${creds.secretKey}'`);
|
|
556
|
+
}
|
|
557
|
+
if (connection.region) {
|
|
558
|
+
sets.push(`SET s3_region = '${connection.region}'`);
|
|
559
|
+
}
|
|
560
|
+
if (connection.endpoint) {
|
|
561
|
+
const endpoint = connection.endpoint.replace(/^https?:\/\//, '');
|
|
562
|
+
sets.push(`SET s3_endpoint = '${endpoint}'`);
|
|
563
|
+
if (connection.endpoint.startsWith('http://')) {
|
|
564
|
+
sets.push(`SET s3_use_ssl = false`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
// Always use path-style — virtual-hosted breaks for buckets with dots
|
|
568
|
+
sets.push(`SET s3_url_style = 'path'`);
|
|
569
|
+
if (sets.length > 0) {
|
|
570
|
+
const t0 = performance.now();
|
|
571
|
+
await conn.query(`${sets.join('; ')};`);
|
|
572
|
+
log(`configureStorage → ${sets.length} SETs batched in ${elapsed(t0)} (provider: ${connection.provider ?? 's3'})`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
catch (err) {
|
|
576
|
+
console.error(LOG_PREFIX, 'configureStorage error:', err);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
async detectCrs(connId, path, geomCol) {
|
|
580
|
+
const t0 = performance.now();
|
|
581
|
+
log(`detectCrs → standalone call for "${geomCol}"`, path);
|
|
582
|
+
const db = await getDB();
|
|
583
|
+
const conn = await db.connect();
|
|
584
|
+
await ensureGeoConversionDisabled(conn);
|
|
585
|
+
try {
|
|
586
|
+
if (connId) {
|
|
587
|
+
await this.configureStorage(conn, connId);
|
|
588
|
+
}
|
|
589
|
+
const crs = await this.detectCrsWithConn(conn, path, geomCol);
|
|
590
|
+
log(`detectCrs → ${crs ?? 'WGS84/null'} in ${elapsed(t0)}`);
|
|
591
|
+
return crs;
|
|
592
|
+
}
|
|
593
|
+
catch (err) {
|
|
594
|
+
logWarn('detectCrs → failed:', err);
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
finally {
|
|
598
|
+
await conn.close();
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
async detectCrsWithConn(conn, path, geomCol) {
|
|
602
|
+
// Strategy 1: GeoParquet file-level metadata (geo key in KV metadata)
|
|
603
|
+
try {
|
|
604
|
+
const t1 = performance.now();
|
|
605
|
+
const kvResult = await conn.query(`SELECT value FROM parquet_kv_metadata('${path}') WHERE CAST(key AS VARCHAR) = 'geo'`);
|
|
606
|
+
const kvRows = kvResult.toArray();
|
|
607
|
+
log(`detectCrs strategy 1 (kv_metadata) → ${kvRows.length} rows in ${elapsed(t1)}`);
|
|
608
|
+
if (kvRows.length > 0) {
|
|
609
|
+
const raw = kvRows[0].value;
|
|
610
|
+
const text = typeof raw === 'string' ? raw : new TextDecoder().decode(raw);
|
|
611
|
+
const geo = JSON.parse(text);
|
|
612
|
+
const colMeta = geo.columns?.[geomCol] ?? (geo.columns ? Object.values(geo.columns)[0] : null);
|
|
613
|
+
if (colMeta) {
|
|
614
|
+
// GeoParquet "geo" metadata is authoritative:
|
|
615
|
+
// has crs → extract EPSG; no crs or crs: null → WGS84 per spec
|
|
616
|
+
const result = colMeta.crs ? extractEpsgFromProjjson(colMeta.crs) : null;
|
|
617
|
+
log(`detectCrs strategy 1 → authoritative: ${result ?? 'WGS84/null'} (skipping strategy 2)`);
|
|
618
|
+
return result;
|
|
619
|
+
}
|
|
620
|
+
log('detectCrs strategy 1 → "geo" key found but no column metadata for', geomCol);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
catch (err) {
|
|
624
|
+
log('detectCrs strategy 1 → skipped (not GeoParquet or no KV metadata):', err?.message ?? err);
|
|
625
|
+
}
|
|
626
|
+
// Strategy 2: Native Parquet GEOMETRY type (Parquet Format 2.11+)
|
|
627
|
+
// CRS is in the schema logical_type: GeometryType(crs=...)
|
|
628
|
+
try {
|
|
629
|
+
const t2 = performance.now();
|
|
630
|
+
const schemaResult = await conn.query(`SELECT logical_type FROM parquet_schema('${path}') WHERE name = '${geomCol}'`);
|
|
631
|
+
const schemaRows = schemaResult.toArray();
|
|
632
|
+
log(`detectCrs strategy 2 (parquet_schema) → ${schemaRows.length} rows in ${elapsed(t2)}`);
|
|
633
|
+
if (schemaRows.length > 0) {
|
|
634
|
+
const logicalType = String(schemaRows[0].logical_type ?? '');
|
|
635
|
+
log(`detectCrs strategy 2 → logical_type: "${logicalType}"`);
|
|
636
|
+
const epsg = await extractCrsFromLogicalType(logicalType, conn, path);
|
|
637
|
+
if (epsg) {
|
|
638
|
+
log(`detectCrs strategy 2 → found: ${epsg}`);
|
|
639
|
+
return epsg;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
catch (err) {
|
|
644
|
+
log('detectCrs strategy 2 → skipped:', err?.message ?? err);
|
|
645
|
+
}
|
|
646
|
+
log('detectCrs → no CRS found, assuming WGS84');
|
|
647
|
+
return null;
|
|
648
|
+
}
|
|
649
|
+
queryCancellable(connId, sql) {
|
|
650
|
+
let cancelled = false;
|
|
651
|
+
let conn = null;
|
|
652
|
+
const result = (async () => {
|
|
653
|
+
const t0 = performance.now();
|
|
654
|
+
const sqlPreview = sql.length > 120 ? `${sql.slice(0, 120)}…` : sql;
|
|
655
|
+
log(`queryCancellable → ${sqlPreview}`);
|
|
656
|
+
const db = await getDB();
|
|
657
|
+
conn = await db.connect();
|
|
658
|
+
await ensureGeoConversionDisabled(conn);
|
|
659
|
+
log(`queryCancellable → connected in ${elapsed(t0)}`);
|
|
660
|
+
try {
|
|
661
|
+
if (connId) {
|
|
662
|
+
await this.configureStorage(conn, connId);
|
|
663
|
+
}
|
|
664
|
+
const tQuery = performance.now();
|
|
665
|
+
const reader = await conn.send(sql);
|
|
666
|
+
log(`queryCancellable → send() in ${elapsed(tQuery)}`);
|
|
667
|
+
const rows = [];
|
|
668
|
+
let cols = [];
|
|
669
|
+
let types = [];
|
|
670
|
+
const batches = reader[Symbol.asyncIterator]();
|
|
671
|
+
let first = true;
|
|
672
|
+
while (true) {
|
|
673
|
+
if (cancelled)
|
|
674
|
+
throw new QueryCancelledError();
|
|
675
|
+
const { value: batch, done } = await batches.next();
|
|
676
|
+
if (done)
|
|
677
|
+
break;
|
|
678
|
+
if (first && batch.schema) {
|
|
679
|
+
cols = batch.schema.fields.map((f) => f.name);
|
|
680
|
+
types = batch.schema.fields.map((f) => String(f.type));
|
|
681
|
+
first = false;
|
|
682
|
+
}
|
|
683
|
+
for (const row of batch.toArray()) {
|
|
684
|
+
const json = typeof row.toJSON === 'function' ? row.toJSON() : { ...row };
|
|
685
|
+
// Binary columns return Uint8Array views into the Arrow
|
|
686
|
+
// RecordBatch buffer. The streaming reader can reclaim/reuse
|
|
687
|
+
// buffers between batches (especially for cached queries),
|
|
688
|
+
// making stale views. Copy them so row data survives.
|
|
689
|
+
for (const key in json) {
|
|
690
|
+
if (json[key] instanceof Uint8Array) {
|
|
691
|
+
json[key] = json[key].slice();
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
rows.push(json);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
log(`queryCancellable → done in ${elapsed(t0)}, ${rows.length} rows`);
|
|
698
|
+
return { columns: cols, types, rowCount: rows.length, rows };
|
|
699
|
+
}
|
|
700
|
+
catch (err) {
|
|
701
|
+
if (cancelled || err instanceof QueryCancelledError) {
|
|
702
|
+
log(`queryCancellable → cancelled after ${elapsed(t0)}`);
|
|
703
|
+
throw new QueryCancelledError();
|
|
704
|
+
}
|
|
705
|
+
logWarn(`queryCancellable → failed after ${elapsed(t0)}:`, err?.message ?? err);
|
|
706
|
+
throw err;
|
|
707
|
+
}
|
|
708
|
+
finally {
|
|
709
|
+
await conn?.close();
|
|
710
|
+
conn = null;
|
|
711
|
+
}
|
|
712
|
+
})();
|
|
713
|
+
const cancel = async () => {
|
|
714
|
+
cancelled = true;
|
|
715
|
+
try {
|
|
716
|
+
if (conn)
|
|
717
|
+
await conn.cancelSent();
|
|
718
|
+
return true;
|
|
719
|
+
}
|
|
720
|
+
catch {
|
|
721
|
+
return false;
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
return { result, cancel };
|
|
725
|
+
}
|
|
726
|
+
queryForMapCancellable(connId, sql, geomCol, geomColType, sourceCrs) {
|
|
727
|
+
let cancelled = false;
|
|
728
|
+
let conn = null;
|
|
729
|
+
const result = (async () => {
|
|
730
|
+
const t0 = performance.now();
|
|
731
|
+
log(`queryForMapCancellable → geomCol: ${geomCol}, type: ${geomColType}, crs: ${sourceCrs ?? 'WGS84'}`);
|
|
732
|
+
const db = await getDB();
|
|
733
|
+
conn = await db.connect();
|
|
734
|
+
await ensureGeoConversionDisabled(conn);
|
|
735
|
+
try {
|
|
736
|
+
if (connId) {
|
|
737
|
+
await this.configureStorage(conn, connId);
|
|
738
|
+
}
|
|
739
|
+
// Build geometry expression (same logic as queryForMap)
|
|
740
|
+
const quoted = `"${geomCol}"`;
|
|
741
|
+
const upper = geomColType.toUpperCase();
|
|
742
|
+
const isSpatialType = upper === 'GEOMETRY' ||
|
|
743
|
+
upper === 'GEOGRAPHY' ||
|
|
744
|
+
upper === 'WKB_BLOB' ||
|
|
745
|
+
upper.includes('POINT') ||
|
|
746
|
+
upper.includes('LINESTRING') ||
|
|
747
|
+
upper.includes('POLYGON') ||
|
|
748
|
+
upper.includes('BINARY');
|
|
749
|
+
const isWkbBlob = upper === 'BLOB' || upper === 'BYTEA';
|
|
750
|
+
let wkbExpr;
|
|
751
|
+
let geomExpr;
|
|
752
|
+
if (isWkbBlob && !sourceCrs) {
|
|
753
|
+
// Already WKB — use directly, no spatial function calls needed.
|
|
754
|
+
wkbExpr = quoted;
|
|
755
|
+
geomExpr = null; // geometry type detected client-side from WKB headers
|
|
756
|
+
}
|
|
757
|
+
else {
|
|
758
|
+
geomExpr = isSpatialType
|
|
759
|
+
? quoted
|
|
760
|
+
: isWkbBlob
|
|
761
|
+
? `ST_GeomFromWKB(${quoted})`
|
|
762
|
+
: `ST_GeomFromGeoJSON(${quoted})`;
|
|
763
|
+
if (sourceCrs) {
|
|
764
|
+
geomExpr = `ST_Transform(${geomExpr}, '${sourceCrs}', 'EPSG:4326', always_xy := true)`;
|
|
765
|
+
}
|
|
766
|
+
wkbExpr = `ST_AsWKB(${geomExpr})`;
|
|
767
|
+
}
|
|
768
|
+
const mapSql = geomExpr
|
|
769
|
+
? `SELECT *, ${wkbExpr} AS __wkb, ST_GeometryType(${geomExpr}) AS __geom_type FROM (${sql}) __src`
|
|
770
|
+
: `SELECT *, ${wkbExpr} AS __wkb FROM (${sql}) __src`;
|
|
771
|
+
const reader = await conn.send(mapSql);
|
|
772
|
+
const wkbArrays = [];
|
|
773
|
+
let geometryType = 'POINT';
|
|
774
|
+
let geometryTypeDetected = false;
|
|
775
|
+
const skipCols = new Set([geomCol, '__wkb', '__geom_type']);
|
|
776
|
+
const attributes = new Map();
|
|
777
|
+
let fieldsInitialized = false;
|
|
778
|
+
const fieldNames = [];
|
|
779
|
+
const fieldTypes = new Map();
|
|
780
|
+
const batches = reader[Symbol.asyncIterator]();
|
|
781
|
+
while (true) {
|
|
782
|
+
if (cancelled)
|
|
783
|
+
throw new QueryCancelledError();
|
|
784
|
+
const { value: batch, done } = await batches.next();
|
|
785
|
+
if (done)
|
|
786
|
+
break;
|
|
787
|
+
if (!fieldsInitialized && batch.schema) {
|
|
788
|
+
for (const field of batch.schema.fields) {
|
|
789
|
+
const typeStr = String(field.type);
|
|
790
|
+
if (skipCols.has(field.name))
|
|
791
|
+
continue;
|
|
792
|
+
// Skip binary/blob columns — not useful for map tooltips
|
|
793
|
+
if (isBinaryType(typeStr))
|
|
794
|
+
continue;
|
|
795
|
+
fieldNames.push(field.name);
|
|
796
|
+
fieldTypes.set(field.name, typeStr);
|
|
797
|
+
attributes.set(field.name, { values: [], type: typeStr });
|
|
798
|
+
}
|
|
799
|
+
fieldsInitialized = true;
|
|
800
|
+
}
|
|
801
|
+
// Extract WKB and geometry type from batch
|
|
802
|
+
const wkbCol = batch.getChild('__wkb');
|
|
803
|
+
const typeCol = batch.getChild('__geom_type');
|
|
804
|
+
for (let i = 0; i < batch.numRows; i++) {
|
|
805
|
+
const v = wkbCol?.get(i);
|
|
806
|
+
// .get(i) returns a Uint8Array view into the batch buffer —
|
|
807
|
+
// copy it so data survives buffer reuse across batches.
|
|
808
|
+
if (v)
|
|
809
|
+
wkbArrays.push(v instanceof Uint8Array ? v.slice() : new Uint8Array(v));
|
|
810
|
+
if (!geometryTypeDetected && typeCol) {
|
|
811
|
+
const t = typeCol.get(i);
|
|
812
|
+
if (t) {
|
|
813
|
+
geometryType = String(t);
|
|
814
|
+
geometryTypeDetected = true;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
// Extract attribute columns — type-aware bulk extraction
|
|
819
|
+
for (const name of fieldNames) {
|
|
820
|
+
const col = batch.getChild(name);
|
|
821
|
+
if (!col)
|
|
822
|
+
continue;
|
|
823
|
+
const attr = attributes.get(name);
|
|
824
|
+
appendColumnBulk(attr.values, col, batch.numRows, fieldTypes.get(name));
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
log(`queryForMapCancellable → done in ${elapsed(t0)}, ${wkbArrays.length} geometries (${geometryType})`);
|
|
828
|
+
return { wkbArrays, geometryType, attributes, rowCount: wkbArrays.length };
|
|
829
|
+
}
|
|
830
|
+
catch (err) {
|
|
831
|
+
if (cancelled || err instanceof QueryCancelledError) {
|
|
832
|
+
log(`queryForMapCancellable → cancelled after ${elapsed(t0)}`);
|
|
833
|
+
throw new QueryCancelledError();
|
|
834
|
+
}
|
|
835
|
+
logWarn(`queryForMapCancellable → failed after ${elapsed(t0)}:`, err?.message ?? err);
|
|
836
|
+
throw err;
|
|
837
|
+
}
|
|
838
|
+
finally {
|
|
839
|
+
await conn?.close();
|
|
840
|
+
conn = null;
|
|
841
|
+
}
|
|
842
|
+
})();
|
|
843
|
+
const cancel = async () => {
|
|
844
|
+
cancelled = true;
|
|
845
|
+
try {
|
|
846
|
+
if (conn)
|
|
847
|
+
await conn.cancelSent();
|
|
848
|
+
return true;
|
|
849
|
+
}
|
|
850
|
+
catch {
|
|
851
|
+
return false;
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
return { result, cancel };
|
|
855
|
+
}
|
|
856
|
+
async forceCancel() {
|
|
857
|
+
log('forceCancel → terminating worker');
|
|
858
|
+
try {
|
|
859
|
+
if (dbPromise) {
|
|
860
|
+
const db = await dbPromise;
|
|
861
|
+
await db.terminate();
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
catch (err) {
|
|
865
|
+
logWarn('forceCancel → terminate error:', err);
|
|
866
|
+
}
|
|
867
|
+
finally {
|
|
868
|
+
dbPromise = null;
|
|
869
|
+
geoConversionGlobal = false;
|
|
870
|
+
log('forceCancel → done, next getDB() will reinitialize');
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
async releaseMemory() {
|
|
874
|
+
const db = await getDB();
|
|
875
|
+
const conn = await db.connect();
|
|
876
|
+
try {
|
|
877
|
+
await conn.query('CALL pragma_database_size()');
|
|
878
|
+
await conn.query('CHECKPOINT');
|
|
879
|
+
}
|
|
880
|
+
catch {
|
|
881
|
+
// Ignore — checkpoint may fail on read-only/in-memory DBs
|
|
882
|
+
}
|
|
883
|
+
finally {
|
|
884
|
+
await conn.close();
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
async dispose() {
|
|
888
|
+
await this.releaseMemory();
|
|
889
|
+
}
|
|
890
|
+
}
|