@walkthru-earth/objex 1.3.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/LICENSE +5 -0
  2. package/README.md +28 -20
  3. package/dist/components/browser/FileTreeSidebar.svelte +32 -17
  4. package/dist/components/layout/AboutSheet.svelte +5 -2
  5. package/dist/components/layout/ConnectionDialog.svelte +7 -2
  6. package/dist/components/layout/SettingsSheet.svelte +238 -0
  7. package/dist/components/layout/SettingsSheet.svelte.d.ts +6 -0
  8. package/dist/components/layout/Sidebar.svelte +73 -6
  9. package/dist/components/layout/Sidebar.svelte.d.ts +4 -1
  10. package/dist/components/layout/StatusBar.svelte +17 -14
  11. package/dist/components/layout/TabBar.svelte +4 -4
  12. package/dist/components/ui/context-menu/context-menu-radio-group.svelte.d.ts +1 -1
  13. package/dist/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte.d.ts +1 -1
  14. package/dist/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte.d.ts +1 -1
  15. package/dist/components/ui/input/input.svelte.d.ts +1 -1
  16. package/dist/components/ui/resizable/index.d.ts +1 -1
  17. package/dist/components/ui/resizable/index.js +2 -2
  18. package/dist/components/ui/slider/index.d.ts +3 -0
  19. package/dist/components/ui/slider/index.js +5 -0
  20. package/dist/components/ui/slider/range-slider.svelte +94 -0
  21. package/dist/components/ui/slider/range-slider.svelte.d.ts +21 -0
  22. package/dist/components/ui/slider/slider.svelte +83 -0
  23. package/dist/components/ui/slider/slider.svelte.d.ts +7 -0
  24. package/dist/components/viewers/ArchiveViewer.svelte +140 -113
  25. package/dist/components/viewers/CodeViewer.svelte +45 -48
  26. package/dist/components/viewers/CodeViewer.svelte.d.ts +1 -1
  27. package/dist/components/viewers/CogControls.svelte +338 -184
  28. package/dist/components/viewers/CogControls.svelte.d.ts +33 -10
  29. package/dist/components/viewers/CogViewer.svelte +269 -116
  30. package/dist/components/viewers/CopcViewer.svelte +8 -15
  31. package/dist/components/viewers/DatabaseViewer.svelte +22 -21
  32. package/dist/components/viewers/FileInfo.svelte +16 -16
  33. package/dist/components/viewers/FlatGeobufViewer.svelte +16 -46
  34. package/dist/components/viewers/GeoParquetMapViewer.svelte +11 -9
  35. package/dist/components/viewers/GeoParquetMapViewer.svelte.d.ts +1 -1
  36. package/dist/components/viewers/ImageViewer.svelte +12 -14
  37. package/dist/components/viewers/LoadProgress.svelte +6 -6
  38. package/dist/components/viewers/MarkdownViewer.svelte +29 -30
  39. package/dist/components/viewers/MediaViewer.svelte +13 -14
  40. package/dist/components/viewers/ModelViewer.svelte +18 -21
  41. package/dist/components/viewers/MultiCogViewer.svelte +474 -106
  42. package/dist/components/viewers/MultiCogViewer.svelte.d.ts +1 -1
  43. package/dist/components/viewers/NotebookViewer.svelte +28 -29
  44. package/dist/components/viewers/PdfViewer.svelte +24 -33
  45. package/dist/components/viewers/PmtilesViewer.svelte +13 -15
  46. package/dist/components/viewers/QueryHistoryPanel.svelte +18 -18
  47. package/dist/components/viewers/RawViewer.svelte +27 -21
  48. package/dist/components/viewers/StacMapViewer.svelte +6 -13
  49. package/dist/components/viewers/StacMosaicViewer.svelte +1764 -410
  50. package/dist/components/viewers/StacMosaicViewer.svelte.d.ts +1 -1
  51. package/dist/components/viewers/StacTabViewer.svelte +26 -15
  52. package/dist/components/viewers/StacTabViewer.svelte.d.ts +1 -1
  53. package/dist/components/viewers/TableGrid.svelte +38 -34
  54. package/dist/components/viewers/TableStatusBar.svelte +7 -7
  55. package/dist/components/viewers/TableToolbar.svelte +10 -9
  56. package/dist/components/viewers/TableViewer.svelte +47 -30
  57. package/dist/components/viewers/TableViewer.svelte.d.ts +1 -0
  58. package/dist/components/viewers/ViewerHeader.svelte +18 -0
  59. package/dist/components/viewers/ViewerHeader.svelte.d.ts +10 -0
  60. package/dist/components/viewers/ViewerRouter.svelte +16 -8
  61. package/dist/components/viewers/ViewerStatus.svelte +19 -0
  62. package/dist/components/viewers/ViewerStatus.svelte.d.ts +7 -0
  63. package/dist/components/viewers/ZarrMapViewer.svelte +24 -21
  64. package/dist/components/viewers/ZarrViewer.svelte +98 -65
  65. package/dist/components/viewers/cog/ChannelPicker.svelte +83 -0
  66. package/dist/components/viewers/cog/ChannelPicker.svelte.d.ts +13 -0
  67. package/dist/components/viewers/cog/PixelInspectorPanel.svelte +87 -0
  68. package/dist/components/viewers/cog/PixelInspectorPanel.svelte.d.ts +17 -0
  69. package/dist/components/viewers/cog/buildRgbLayer.d.ts +78 -0
  70. package/dist/components/viewers/cog/buildRgbLayer.js +176 -0
  71. package/dist/components/viewers/map/AttributeTable.svelte +7 -7
  72. package/dist/components/viewers/map/MapContainer.svelte +38 -12
  73. package/dist/components/viewers/pmtiles/PmtilesArchiveView.svelte +109 -83
  74. package/dist/components/viewers/pmtiles/PmtilesTileInspector.svelte +16 -16
  75. package/dist/components/viewers/stac/StacDatetimeBar.svelte +175 -0
  76. package/dist/components/viewers/stac/StacDatetimeBar.svelte.d.ts +10 -0
  77. package/dist/components/viewers/stac/StacFilterPanel.svelte +243 -0
  78. package/dist/components/viewers/stac/StacFilterPanel.svelte.d.ts +14 -0
  79. package/dist/components/viewers/stac/StacItemInspector.svelte +223 -0
  80. package/dist/components/viewers/stac/StacItemInspector.svelte.d.ts +10 -0
  81. package/dist/components/viewers/stac/StacItemStrip.svelte +228 -0
  82. package/dist/components/viewers/stac/StacItemStrip.svelte.d.ts +12 -0
  83. package/dist/constants.d.ts +6 -0
  84. package/dist/constants.js +8 -0
  85. package/dist/file-icons/index.d.ts +1 -1
  86. package/dist/file-icons/index.js +1 -1
  87. package/dist/i18n/ar.js +113 -2
  88. package/dist/i18n/en.js +113 -2
  89. package/dist/index.d.ts +2 -28
  90. package/dist/index.js +7 -23
  91. package/dist/query/engine.d.ts +10 -0
  92. package/dist/query/source.js +1 -1
  93. package/dist/query/stac-source-factory.d.ts +65 -0
  94. package/dist/query/stac-source-factory.js +77 -0
  95. package/dist/query/stac-source-parquet.d.ts +135 -0
  96. package/dist/query/stac-source-parquet.js +468 -0
  97. package/dist/query/wasm.d.ts +8 -0
  98. package/dist/query/wasm.js +310 -65
  99. package/dist/storage/presign.js +3 -2
  100. package/dist/storage/providers.js +7 -6
  101. package/dist/stores/config.svelte.d.ts +15 -0
  102. package/dist/stores/config.svelte.js +46 -0
  103. package/dist/stores/connections.svelte.d.ts +2 -2
  104. package/dist/stores/connections.svelte.js +1 -2
  105. package/dist/stores/files.svelte.d.ts +1 -1
  106. package/dist/stores/files.svelte.js +1 -1
  107. package/dist/stores/query-history.svelte.js +1 -1
  108. package/dist/stores/settings.svelte.d.ts +16 -1
  109. package/dist/stores/settings.svelte.js +104 -48
  110. package/dist/stores/tabs.svelte.d.ts +3 -0
  111. package/dist/stores/tabs.svelte.js +17 -0
  112. package/dist/utils/cog-histogram.d.ts +121 -0
  113. package/dist/utils/cog-histogram.js +424 -0
  114. package/dist/utils/cog.d.ts +177 -20
  115. package/dist/utils/cog.js +361 -76
  116. package/dist/utils/colormap-sprite.d.ts +0 -9
  117. package/dist/utils/colormap-sprite.js +0 -21
  118. package/dist/utils/deck.d.ts +18 -12
  119. package/dist/utils/deck.js +15 -7
  120. package/dist/utils/media-query.svelte.d.ts +14 -0
  121. package/dist/utils/media-query.svelte.js +29 -0
  122. package/dist/utils/pmtiles-tile.js +2 -2
  123. package/dist/utils/signed-url-effect.d.ts +7 -0
  124. package/dist/utils/signed-url-effect.js +19 -0
  125. package/dist/utils/{url.d.ts → signed-url.d.ts} +15 -1
  126. package/dist/utils/{url.js → signed-url.js} +32 -10
  127. package/dist/utils/url-state.d.ts +36 -0
  128. package/dist/utils/url-state.js +72 -2
  129. package/dist/utils/zarr-tab.d.ts +1 -2
  130. package/dist/utils/zarr-tab.js +1 -2
  131. package/dist/utils/zarr.d.ts +0 -17
  132. package/dist/utils/zarr.js +1 -45
  133. package/package.json +55 -84
  134. package/dist/components/browser/Breadcrumb.svelte +0 -50
  135. package/dist/components/browser/Breadcrumb.svelte.d.ts +0 -7
  136. package/dist/components/browser/CreateFolderDialog.svelte +0 -98
  137. package/dist/components/browser/CreateFolderDialog.svelte.d.ts +0 -6
  138. package/dist/components/browser/DeleteConfirmDialog.svelte +0 -90
  139. package/dist/components/browser/DeleteConfirmDialog.svelte.d.ts +0 -8
  140. package/dist/components/browser/DropZone.svelte +0 -83
  141. package/dist/components/browser/DropZone.svelte.d.ts +0 -7
  142. package/dist/components/browser/FileBrowser.svelte +0 -252
  143. package/dist/components/browser/FileBrowser.svelte.d.ts +0 -3
  144. package/dist/components/browser/FileRow.svelte +0 -117
  145. package/dist/components/browser/FileRow.svelte.d.ts +0 -9
  146. package/dist/components/browser/RenameDialog.svelte +0 -101
  147. package/dist/components/browser/RenameDialog.svelte.d.ts +0 -8
  148. package/dist/components/browser/SearchBar.svelte +0 -40
  149. package/dist/components/browser/SearchBar.svelte.d.ts +0 -6
  150. package/dist/components/browser/UploadButton.svelte +0 -65
  151. package/dist/components/browser/UploadButton.svelte.d.ts +0 -3
  152. package/dist/query/stac-geoparquet.d.ts +0 -31
  153. package/dist/query/stac-geoparquet.js +0 -136
  154. package/dist/utils/clipboard.d.ts +0 -13
  155. package/dist/utils/clipboard.js +0 -38
  156. package/dist/utils/cloud-url.d.ts +0 -27
  157. package/dist/utils/cloud-url.js +0 -61
  158. package/dist/utils/cog-pure.d.ts +0 -25
  159. package/dist/utils/cog-pure.js +0 -35
  160. package/dist/utils/column-types.d.ts +0 -5
  161. package/dist/utils/column-types.js +0 -137
  162. package/dist/utils/connection-identity.d.ts +0 -51
  163. package/dist/utils/connection-identity.js +0 -97
  164. package/dist/utils/error.d.ts +0 -8
  165. package/dist/utils/error.js +0 -12
  166. package/dist/utils/evidence-context.d.ts +0 -22
  167. package/dist/utils/evidence-context.js +0 -56
  168. package/dist/utils/export.d.ts +0 -22
  169. package/dist/utils/export.js +0 -76
  170. package/dist/utils/file-sort.d.ts +0 -20
  171. package/dist/utils/file-sort.js +0 -41
  172. package/dist/utils/format.d.ts +0 -24
  173. package/dist/utils/format.js +0 -78
  174. package/dist/utils/geoarrow.d.ts +0 -32
  175. package/dist/utils/geoarrow.js +0 -672
  176. package/dist/utils/geometry-type.d.ts +0 -52
  177. package/dist/utils/geometry-type.js +0 -76
  178. package/dist/utils/hex.d.ts +0 -10
  179. package/dist/utils/hex.js +0 -27
  180. package/dist/utils/host-detection.d.ts +0 -23
  181. package/dist/utils/host-detection.js +0 -95
  182. package/dist/utils/local-storage.d.ts +0 -16
  183. package/dist/utils/local-storage.js +0 -37
  184. package/dist/utils/markdown-sql.d.ts +0 -30
  185. package/dist/utils/markdown-sql.js +0 -72
  186. package/dist/utils/notebook.d.ts +0 -59
  187. package/dist/utils/notebook.js +0 -211
  188. package/dist/utils/parquet-metadata.d.ts +0 -64
  189. package/dist/utils/parquet-metadata.js +0 -262
  190. package/dist/utils/stac-geoparquet.d.ts +0 -90
  191. package/dist/utils/stac-geoparquet.js +0 -223
  192. package/dist/utils/stac-hydrate.d.ts +0 -38
  193. package/dist/utils/stac-hydrate.js +0 -243
  194. package/dist/utils/stac.d.ts +0 -136
  195. package/dist/utils/stac.js +0 -176
  196. package/dist/utils/storage-url.d.ts +0 -90
  197. package/dist/utils/storage-url.js +0 -568
  198. package/dist/utils/wkb.d.ts +0 -43
  199. package/dist/utils/wkb.js +0 -359
@@ -0,0 +1,468 @@
1
+ /**
2
+ * stac-geoparquet implementation of the StacSource contract.
3
+ *
4
+ * Reuses:
5
+ * - `getQueryEngine()` + `queryCancellable`/`query` for the single worker
6
+ * - `resolveTableSourceAsync(tab)` for presigned `signed-s3` URL handling
7
+ * - `stacRowToItem` from `@walkthru-earth/objex-utils` for the pure transform
8
+ * - `parseWKB` from `@walkthru-earth/objex-utils` for geometry decoding
9
+ *
10
+ * Push-down: `bbox` (`ST_Intersects` + `ST_MakeEnvelope`) and `datetime`
11
+ * (`datetime BETWEEN TIMESTAMPTZ ...`). Without the datetime push-down,
12
+ * `LIMIT + ORDER BY datetime DESC` silently drops older rows before the
13
+ * client-side filter ever runs, so any window outside the freshest N items
14
+ * returned zero matches. Cloud cover / GSD / platform / etc. still ride
15
+ * along on the residual until slice 3 plumbs them through DuckDB SQL.
16
+ *
17
+ * Hive partitioning: when the factory (or an SDK caller) sets
18
+ * `useHivePartitioning: true`, the FROM target switches to
19
+ * `read_parquet('.../**\/*.parquet', hive_partitioning=true,
20
+ * union_by_name=true)`. Mirrors lazycogs'
21
+ * `DuckdbClient(use_hive_partitioning=True)`. Partition columns appear as
22
+ * virtual columns on the schema, but `buildSelectList` only projects known
23
+ * STAC columns so they never leak into the rendered Items. `union_by_name`
24
+ * is required because partitioned writes can drift schemas across
25
+ * partitions (extra `proj:*` columns added later, etc.).
26
+ *
27
+ * Yields a single batch with `done: true`. Slice 3 turns this into a real
28
+ * stream via `conn.send()` so large catalogs can render progressively.
29
+ */
30
+ import { DEFAULT_APP_CONFIG, emptyPushdown, parseWKB, stacRowToItem } from '@walkthru-earth/objex-utils';
31
+ import { QueryCancelledError } from './engine.js';
32
+ import { getQueryEngine } from './index.js';
33
+ import { resolveTableSourceAsync } from './source.js';
34
+ /**
35
+ * Default mobile detection used when `lowMemoryMode` is not explicitly set.
36
+ * iOS Safari caps the WASM heap at ~1.8 GiB and rarely engages OPFS spill
37
+ * (`credentialless` COEP only landed in 17.6), so STRUCT-heavy stac-geoparquet
38
+ * scans OOM during the parquet decode before any rows reach the consumer.
39
+ * Evaluated per source construction so a device that rotates or resizes re-checks.
40
+ * Desktop browsers are never classified low-memory regardless of window size.
41
+ */
42
+ function detectLowMemoryDefault() {
43
+ if (typeof navigator === 'undefined')
44
+ return false;
45
+ const isMobileUa = /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
46
+ if (!isMobileUa)
47
+ return false; // desktop is never low-memory, regardless of window size
48
+ if (typeof window === 'undefined')
49
+ return true;
50
+ return Math.min(window.innerWidth, window.innerHeight) <= 820;
51
+ }
52
+ /**
53
+ * Build a SQL fragment for the datetime filter or return `null` when nothing
54
+ * is selected. Each bound is round-tripped through `Date.parse` + `toISOString`
55
+ * so a malformed input is dropped instead of being inlined into SQL.
56
+ *
57
+ * The STAC item-properties spec lets a row carry either a single `datetime`
58
+ * timestamp OR a `start_datetime`+`end_datetime` interval (Landsat composites,
59
+ * climate reanalysis, etc.). When the parquet schema exposes the interval
60
+ * columns we widen the predicate so interval-only rows are not silently
61
+ * excluded by the simpler `datetime BETWEEN ...` form.
62
+ */
63
+ function buildDatetimeWhere(filter, available) {
64
+ if (!filter)
65
+ return null;
66
+ const minIso = filter.min && Number.isFinite(Date.parse(filter.min))
67
+ ? new Date(Date.parse(filter.min)).toISOString()
68
+ : null;
69
+ const maxIso = filter.max && Number.isFinite(Date.parse(filter.max))
70
+ ? new Date(Date.parse(filter.max)).toISOString()
71
+ : null;
72
+ if (!minIso && !maxIso)
73
+ return null;
74
+ // Predicate matching a single `datetime` column.
75
+ const dtParts = [];
76
+ if (minIso)
77
+ dtParts.push(`datetime >= TIMESTAMPTZ '${minIso}'`);
78
+ if (maxIso)
79
+ dtParts.push(`datetime <= TIMESTAMPTZ '${maxIso}'`);
80
+ const dtClause = dtParts.length > 0 ? dtParts.join(' AND ') : null;
81
+ // Predicate matching the interval form: an item's [start, end] overlaps the
82
+ // requested window when start <= max AND end >= min.
83
+ const intervalParts = [];
84
+ if (maxIso)
85
+ intervalParts.push(`start_datetime <= TIMESTAMPTZ '${maxIso}'`);
86
+ if (minIso)
87
+ intervalParts.push(`end_datetime >= TIMESTAMPTZ '${minIso}'`);
88
+ const intervalClause = available.startDatetime && available.endDatetime && intervalParts.length > 0
89
+ ? intervalParts.join(' AND ')
90
+ : null;
91
+ if (available.datetime && intervalClause && dtClause) {
92
+ // Either a row's `datetime` falls in the window, or the item carries an
93
+ // interval that overlaps it. NULL `datetime` rows are excluded by the
94
+ // first branch (NULL comparisons are NULL/false), but the second branch
95
+ // catches them via the interval columns.
96
+ return `((${dtClause}) OR (${intervalClause}))`;
97
+ }
98
+ if (available.datetime && dtClause)
99
+ return dtClause;
100
+ if (intervalClause)
101
+ return intervalClause;
102
+ return null;
103
+ }
104
+ function buildBboxWhere(bbox) {
105
+ if (!bbox || bbox.length !== 4 || !bbox.every((n) => Number.isFinite(n)))
106
+ return null;
107
+ const [w, s, e, n] = bbox;
108
+ return `ST_Intersects(geometry, ST_MakeEnvelope(${w}, ${s}, ${e}, ${n}))`;
109
+ }
110
+ function joinWhere(parts) {
111
+ const live = parts.filter((p) => p !== null && p.length > 0);
112
+ return live.length === 0 ? '' : ` WHERE ${live.join(' AND ')}`;
113
+ }
114
+ const DEFAULT_LIMIT = DEFAULT_APP_CONFIG.defaults.mosaicItemLimit;
115
+ /**
116
+ * Build the SELECT list. All columns are optional in the stac-geoparquet
117
+ * spec, so we only project what we know we'll use and the spec requires.
118
+ * The optional `proj:*` / `raster:*` / `bands` columns are sniffed from the
119
+ * schema so missing columns don't trigger a DuckDB binder error.
120
+ */
121
+ function buildSelectList(availableColumns) {
122
+ const required = [
123
+ 'id',
124
+ 'collection',
125
+ 'type',
126
+ 'stac_version',
127
+ 'stac_extensions',
128
+ 'assets',
129
+ 'bbox',
130
+ 'links'
131
+ ];
132
+ const optional = [
133
+ 'datetime',
134
+ 'start_datetime',
135
+ 'end_datetime',
136
+ 'created',
137
+ 'updated',
138
+ 'eo:cloud_cover',
139
+ 'gsd',
140
+ 'platform',
141
+ 'constellation',
142
+ 'instruments',
143
+ 'proj:code',
144
+ 'proj:bbox',
145
+ 'proj:transform',
146
+ 'proj:shape',
147
+ 'raster:spatial_resolution',
148
+ 'bands'
149
+ ];
150
+ const cols = [];
151
+ for (const name of required) {
152
+ if (availableColumns.has(name))
153
+ cols.push(quoteIdent(name));
154
+ }
155
+ for (const name of optional) {
156
+ if (availableColumns.has(name))
157
+ cols.push(quoteIdent(name));
158
+ }
159
+ // Always project geometry as WKB so parseWKB can decode it regardless of
160
+ // whether DuckDB presents it as the v1.5 GEOMETRY type or a plain BLOB.
161
+ if (availableColumns.has('geometry')) {
162
+ cols.push('ST_AsWKB(geometry) AS geom_wkb');
163
+ }
164
+ return cols.join(', ');
165
+ }
166
+ function quoteIdent(name) {
167
+ return `"${name.replace(/"/g, '""')}"`;
168
+ }
169
+ /**
170
+ * Strip a trailing `/` and any URL fragment / query so a directory URL like
171
+ * `s3://bucket/cache/` becomes `s3://bucket/cache`. The `**\/*.parquet` glob
172
+ * is then appended for the hive read_parquet call.
173
+ */
174
+ function trimDirectoryUrl(url) {
175
+ const noQuery = url.split('?')[0].split('#')[0];
176
+ return noQuery.endsWith('/') ? noQuery.slice(0, -1) : noQuery;
177
+ }
178
+ /**
179
+ * Build the FROM-clause target for a hive-partitioned parquet directory.
180
+ * `union_by_name=true` is required because partitioned writes can drift
181
+ * schemas across partitions (extra `proj:*` columns added later, etc.) and
182
+ * positional union would error out on the first mismatch.
183
+ */
184
+ function buildHiveReadParquet(directoryUrl) {
185
+ const root = trimDirectoryUrl(directoryUrl);
186
+ const escaped = root.replace(/'/g, "''");
187
+ return `read_parquet('${escaped}/**/*.parquet', hive_partitioning=true, union_by_name=true)`;
188
+ }
189
+ /**
190
+ * Best-effort confirmation that a directory contains at least one parquet
191
+ * file. Returns true on the first match. Listing failures fall back to
192
+ * `true` so we still attempt the hive query — DuckDB will surface the real
193
+ * error if the path is empty. Adapters that don't list (UrlAdapter) return
194
+ * an empty array, in which case we also fall through to `true`.
195
+ */
196
+ async function probeHasParquetChild(adapter, tabPath, signal) {
197
+ if (!adapter)
198
+ return true;
199
+ try {
200
+ const entries = await adapter.list(tabPath, signal);
201
+ if (!Array.isArray(entries) || entries.length === 0)
202
+ return true;
203
+ return entries.some((e) => !e.is_dir &&
204
+ (e.extension?.toLowerCase() === 'parquet' ||
205
+ e.extension?.toLowerCase() === 'geoparquet' ||
206
+ e.name?.toLowerCase().endsWith('.parquet') ||
207
+ e.name?.toLowerCase().endsWith('.geoparquet')));
208
+ }
209
+ catch {
210
+ return true;
211
+ }
212
+ }
213
+ /**
214
+ * Build the FROM-clause target. For single-file parquet this is the resolved
215
+ * `read_parquet('url')` from `resolveTableSourceAsync`; for hive directories
216
+ * we override with a recursive glob + `hive_partitioning=true` so DuckDB
217
+ * prunes partition columns from the predicate. `union_by_name=true` is
218
+ * load-bearing — partitioned writes can drift schemas across partitions
219
+ * (extra `proj:*` columns added later, etc.) and positional union would
220
+ * error out on the first mismatch.
221
+ */
222
+ function buildFromTarget(resolved, hive) {
223
+ if (!hive)
224
+ return resolved.ref;
225
+ const url = resolved.fileUrl;
226
+ if (!url) {
227
+ // Hive was requested but we never resolved an httpfs URL (e.g.
228
+ // SQL-backed source). Fall back to the resolved ref — DuckDB will
229
+ // surface the real error if the path can't be globbed.
230
+ return resolved.ref;
231
+ }
232
+ return buildHiveReadParquet(url);
233
+ }
234
+ /**
235
+ * Stream the catalog as Arrow RecordBatches and yield each batch's items as a
236
+ * separate chunk. Peak DuckDB-WASM heap usage tracks one Arrow batch (~64 KiB
237
+ * rows) instead of the full result set; for a 4000-item LIMIT against a
238
+ * stac-geoparquet root with deep `assets` / `bands` payloads this turns the
239
+ * "Out of Memory ... 3.1 GiB / 3.1 GiB used" failure into a steady-state
240
+ * stream that the viewer can also render progressively. Falls back to a
241
+ * single-batch buffered query when the engine has no `queryStream` (test
242
+ * doubles, future engine impls).
243
+ */
244
+ async function* streamQuery(tab, connId, opts) {
245
+ const { signal, limit = DEFAULT_LIMIT, bbox, datetime } = opts;
246
+ const hiveEnabled = opts.hive?.enabled === true;
247
+ if (signal?.aborted)
248
+ throw new QueryCancelledError();
249
+ const engine = await getQueryEngine();
250
+ const resolved = await resolveTableSourceAsync(tab);
251
+ if (signal?.aborted)
252
+ throw new QueryCancelledError();
253
+ const fromTarget = buildFromTarget(resolved, hiveEnabled);
254
+ const schemaSource = hiveEnabled
255
+ ? { ...resolved, ref: fromTarget }
256
+ : resolved;
257
+ const schema = await engine.getSchema(connId, schemaSource);
258
+ if (signal?.aborted)
259
+ throw new QueryCancelledError();
260
+ const available = new Set(schema.map((f) => f.name));
261
+ const selectList = buildSelectList(available);
262
+ if (!available.has('geometry') || !available.has('assets')) {
263
+ throw new Error('Not a stac-geoparquet file (missing geometry or assets column)');
264
+ }
265
+ const datetimeAvailability = {
266
+ datetime: available.has('datetime'),
267
+ startDatetime: available.has('start_datetime'),
268
+ endDatetime: available.has('end_datetime')
269
+ };
270
+ const datetimeWhere = buildDatetimeWhere(datetime, datetimeAvailability);
271
+ const whereClause = joinWhere([buildBboxWhere(bbox), datetimeWhere]);
272
+ // `ORDER BY datetime DESC LIMIT N` is a Top-N: DuckDB still has to read
273
+ // every row's STRUCT `assets` payload before the limit engages. On a
274
+ // mobile WASM heap (~1.8 GiB ceiling, no OPFS spill) that OOMs in the
275
+ // parquet decoder before any rows reach the consumer. `skipOrderBy`
276
+ // trades freshness ordering for early-exit at LIMIT.
277
+ const orderClause = opts.skipOrderBy || !available.has('datetime') ? '' : ' ORDER BY datetime DESC';
278
+ const safeLimit = Math.max(1, Math.floor(Number(limit) || DEFAULT_LIMIT));
279
+ const sql = `SELECT ${selectList} FROM ${fromTarget}${whereClause}${orderClause} LIMIT ${safeLimit}`;
280
+ if (opts.debugExplain) {
281
+ try {
282
+ const plan = (await engine.query(connId, `EXPLAIN ${sql}`));
283
+ // eslint-disable-next-line no-console
284
+ console.debug('[stac-source-parquet] EXPLAIN', { hive: hiveEnabled, sql, plan });
285
+ }
286
+ catch (e) {
287
+ // eslint-disable-next-line no-console
288
+ console.debug('[stac-source-parquet] EXPLAIN failed', e);
289
+ }
290
+ if (signal?.aborted)
291
+ throw new QueryCancelledError();
292
+ }
293
+ const parquetUrl = resolved.fileUrl ?? tab.path;
294
+ const parquetDir = parquetUrl.replace(/[^/]*(?:\?.*)?$/, '');
295
+ const rowToItem = (row) => {
296
+ const id = typeof row.id === 'string' ? row.id : String(row.id ?? '');
297
+ const itemBase = id ? `${parquetDir}${id}/` : parquetUrl;
298
+ return stacRowToItem(row, itemBase, { wkbParser: parseWKB });
299
+ };
300
+ if (engine.queryStream) {
301
+ const stream = engine.queryStream(connId, sql, signal);
302
+ const it = stream[Symbol.asyncIterator]();
303
+ let pending = null;
304
+ while (true) {
305
+ const { value, done } = await it.next();
306
+ if (done)
307
+ break;
308
+ if (signal?.aborted)
309
+ throw new QueryCancelledError();
310
+ const items = value.rows.map(rowToItem);
311
+ // One-batch lookahead so we know which yield is the final one without
312
+ // driving the consumer to track it.
313
+ if (pending)
314
+ yield { items: pending.items, final: false };
315
+ pending = { items };
316
+ }
317
+ yield { items: pending?.items ?? [], final: true };
318
+ return;
319
+ }
320
+ // Fallback: buffered single-batch path (engines without queryStream).
321
+ let resultPromise;
322
+ let cancel = null;
323
+ if (engine.queryCancellable) {
324
+ const handle = engine.queryCancellable(connId, sql);
325
+ cancel = handle.cancel;
326
+ resultPromise = handle.result;
327
+ }
328
+ else {
329
+ resultPromise = engine.query(connId, sql);
330
+ }
331
+ const onAbort = () => {
332
+ cancel?.().catch(() => { });
333
+ };
334
+ signal?.addEventListener('abort', onAbort, { once: true });
335
+ let rows;
336
+ try {
337
+ const result = await resultPromise;
338
+ rows = result.rows ?? [];
339
+ }
340
+ finally {
341
+ signal?.removeEventListener('abort', onAbort);
342
+ }
343
+ if (signal?.aborted)
344
+ throw new QueryCancelledError();
345
+ yield { items: rows.map(rowToItem), final: true };
346
+ }
347
+ /**
348
+ * stac-geoparquet `StacSource`. Slice 1: bbox is the only push-down,
349
+ * single yield with `done: true`. Slice 3 widens push-down (cloud cover /
350
+ * gsd / platform via DuckDB SQL) and turns this into a streaming
351
+ * `conn.send()` cursor.
352
+ *
353
+ * `options.useHivePartitioning` switches the FROM target to a recursive
354
+ * `read_parquet` glob over `tab.path` so DuckDB prunes partitions per
355
+ * `bbox` / `datetime` predicate. The first `query()` call awaits a
356
+ * best-effort `adapter.list()` probe to confirm at least one `.parquet`
357
+ * child exists; if listing fails (e.g. UrlAdapter, AccessDenied) we still
358
+ * attempt the hive query and let DuckDB surface the real error.
359
+ */
360
+ export function createParquetSource(tab, connectionId, options = {}) {
361
+ const requestedHive = options.useHivePartitioning === true;
362
+ const lowMemoryMode = options.lowMemoryMode ?? detectLowMemoryDefault();
363
+ const lowMemoryLimit = Math.max(1, Math.floor(options.lowMemoryLimit ?? 200));
364
+ const capabilities = {
365
+ kind: 'parquet',
366
+ label: requestedHive ? 'stac-geoparquet (hive)' : 'stac-geoparquet',
367
+ countAvailable: true,
368
+ // Now true: `streamQuery` yields one StacSourceBatch per Arrow
369
+ // RecordBatch via the engine's `queryStream` cursor, so peak DuckDB
370
+ // heap usage tracks one batch instead of the full result set. This
371
+ // fixes the `Out of Memory ... in-memory mode` OOM on large catalogs
372
+ // and lets the mosaic render progressively as items arrive.
373
+ streaming: true,
374
+ hivePartitioned: requestedHive,
375
+ pushdown: { ...emptyPushdown(), bbox: true, datetime: true }
376
+ };
377
+ const connId = connectionId;
378
+ // The probe is purely advisory: when `useHivePartitioning: true` is
379
+ // passed, we always run the hive query, but the first probe logs (in
380
+ // debug mode) whether the directory actually has parquet children so a
381
+ // misconfigured path gets a faster signal than DuckDB's binder error.
382
+ // The probe result is cached so a second viewport reload doesn't re-list.
383
+ let hiveProbe = null;
384
+ const ensureHive = async (signal) => {
385
+ if (!requestedHive)
386
+ return false;
387
+ if (!hiveProbe)
388
+ hiveProbe = probeHasParquetChild(options.adapter, tab.path, signal);
389
+ const probed = await hiveProbe;
390
+ if (options.debugExplain && !probed) {
391
+ // eslint-disable-next-line no-console
392
+ console.debug('[stac-source-parquet] hive probe found no .parquet children', {
393
+ path: tab.path
394
+ });
395
+ }
396
+ return true;
397
+ };
398
+ return {
399
+ capabilities,
400
+ async *query(req) {
401
+ if (req.signal.aborted)
402
+ throw new DOMException('Aborted', 'AbortError');
403
+ const hiveEnabled = await ensureHive(req.signal);
404
+ if (req.signal.aborted)
405
+ throw new DOMException('Aborted', 'AbortError');
406
+ const pushedDown = req.filter?.datetime ? { datetime: req.filter.datetime } : {};
407
+ const { datetime: _pushed, ...residualRest } = req.filter ?? {};
408
+ const residual = residualRest;
409
+ let totalSoFar = 0;
410
+ // On mobile, clamp the LIMIT regardless of caller request and
411
+ // drop the ORDER BY so the parquet scan can early-exit. The
412
+ // caller's higher cap (e.g. 2000) would still trigger the
413
+ // 858 MB / 1.8 GiB OOM during STRUCT materialization.
414
+ const effectiveLimit = lowMemoryMode
415
+ ? Math.min(req.limit ?? lowMemoryLimit, lowMemoryLimit)
416
+ : req.limit;
417
+ for await (const chunk of streamQuery(tab, connId, {
418
+ signal: req.signal,
419
+ limit: effectiveLimit,
420
+ bbox: req.bbox,
421
+ datetime: req.filter?.datetime,
422
+ hive: { enabled: hiveEnabled },
423
+ debugExplain: options.debugExplain,
424
+ skipOrderBy: lowMemoryMode
425
+ })) {
426
+ if (req.signal.aborted)
427
+ throw new DOMException('Aborted', 'AbortError');
428
+ totalSoFar += chunk.items.length;
429
+ yield {
430
+ items: chunk.items,
431
+ pushedDown,
432
+ residual,
433
+ done: chunk.final,
434
+ totalHinted: chunk.final ? totalSoFar : undefined
435
+ };
436
+ }
437
+ },
438
+ async count(filter, bbox, signal) {
439
+ if (signal.aborted)
440
+ throw new DOMException('Aborted', 'AbortError');
441
+ const hiveEnabled = await ensureHive(signal);
442
+ if (signal.aborted)
443
+ throw new DOMException('Aborted', 'AbortError');
444
+ const engine = await getQueryEngine();
445
+ const resolved = await resolveTableSourceAsync(tab);
446
+ if (signal.aborted)
447
+ throw new DOMException('Aborted', 'AbortError');
448
+ const fromTarget = buildFromTarget(resolved, hiveEnabled);
449
+ const schemaSource = hiveEnabled
450
+ ? { ...resolved, ref: fromTarget }
451
+ : resolved;
452
+ const schema = await engine.getSchema(connId, schemaSource);
453
+ if (signal.aborted)
454
+ throw new DOMException('Aborted', 'AbortError');
455
+ const available = new Set(schema.map((f) => f.name));
456
+ const datetimeWhere = buildDatetimeWhere(filter?.datetime, {
457
+ datetime: available.has('datetime'),
458
+ startDatetime: available.has('start_datetime'),
459
+ endDatetime: available.has('end_datetime')
460
+ });
461
+ const where = joinWhere([buildBboxWhere(bbox), datetimeWhere]);
462
+ const sql = `SELECT COUNT(*) AS n FROM ${fromTarget}${where}`;
463
+ const result = (await engine.query(connId, sql));
464
+ const raw = result.rows?.[0]?.n ?? 0;
465
+ return typeof raw === 'bigint' ? Number(raw) : Number(raw);
466
+ }
467
+ };
468
+ }
@@ -13,6 +13,14 @@ export declare class WasmQueryEngine implements QueryEngine {
13
13
  detectCrs(connId: string, source: QuerySource, geomCol: string): Promise<string | null>;
14
14
  private detectCrsWithConn;
15
15
  queryCancellable(connId: string, sql: string): QueryHandle;
16
+ /**
17
+ * Streaming variant of `queryCancellable`. Yields one chunk per Arrow
18
+ * RecordBatch so peak memory tracks one batch instead of the full result.
19
+ * Used by `stac-source-parquet` to ingest large catalogs progressively
20
+ * without OOMing the WASM heap. Cancellation routes through `conn.cancelSent`
21
+ * and `signal.aborted`; the connection is always closed in `finally`.
22
+ */
23
+ queryStream(connId: string, sql: string, signal?: AbortSignal): AsyncIterable<QueryResult>;
16
24
  queryForMapCancellable(connId: string, sql: string, geomCol: string, geomColType: string, sourceCrs?: string | null): MapQueryHandle;
17
25
  forceCancel(): Promise<void>;
18
26
  registerFileBuffer(name: string, buffer: Uint8Array): Promise<void>;