@honua/sdk-esri-compat 0.1.12 → 0.1.14

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 (250) hide show
  1. package/README.md +2 -0
  2. package/columnar/apache-arrow.d.ts +17 -0
  3. package/columnar/apache-arrow.d.ts.map +1 -0
  4. package/columnar/apache-arrow.js +1036 -0
  5. package/columnar/batch-cache-conformance.d.ts +66 -0
  6. package/columnar/batch-cache-conformance.d.ts.map +1 -0
  7. package/columnar/batch-cache-conformance.js +317 -0
  8. package/columnar/batch-cache-indexeddb.d.ts +31 -0
  9. package/columnar/batch-cache-indexeddb.d.ts.map +1 -0
  10. package/columnar/batch-cache-indexeddb.js +148 -0
  11. package/columnar/batch-cache.d.ts +298 -0
  12. package/columnar/batch-cache.d.ts.map +1 -0
  13. package/columnar/batch-cache.js +757 -0
  14. package/columnar/geoarrow-aggregate.d.ts +178 -0
  15. package/columnar/geoarrow-aggregate.d.ts.map +1 -0
  16. package/columnar/geoarrow-aggregate.js +976 -0
  17. package/columnar/geoarrow-patch.d.ts +382 -0
  18. package/columnar/geoarrow-patch.d.ts.map +1 -0
  19. package/columnar/geoarrow-patch.js +1656 -0
  20. package/columnar/geoarrow-reproject.d.ts +22 -0
  21. package/columnar/geoarrow-reproject.d.ts.map +1 -0
  22. package/columnar/geoarrow-reproject.js +119 -0
  23. package/columnar/geoarrow-serialization.d.ts +68 -0
  24. package/columnar/geoarrow-serialization.d.ts.map +1 -0
  25. package/columnar/geoarrow-serialization.js +331 -0
  26. package/columnar/geoarrow-types.d.ts +271 -0
  27. package/columnar/geoarrow-types.d.ts.map +1 -0
  28. package/columnar/geoarrow-types.js +15 -0
  29. package/columnar/geoarrow.d.ts +52 -0
  30. package/columnar/geoarrow.d.ts.map +1 -0
  31. package/columnar/geoarrow.js +1469 -0
  32. package/columnar/index.d.ts +43 -0
  33. package/columnar/index.d.ts.map +1 -0
  34. package/columnar/index.js +29 -0
  35. package/columnar/operators.d.ts +70 -0
  36. package/columnar/operators.d.ts.map +1 -0
  37. package/columnar/operators.js +307 -0
  38. package/columnar/result.d.ts +369 -0
  39. package/columnar/result.d.ts.map +1 -0
  40. package/columnar/result.js +815 -0
  41. package/columnar/telemetry.d.ts +152 -0
  42. package/columnar/telemetry.d.ts.map +1 -0
  43. package/columnar/telemetry.js +265 -0
  44. package/columnar/transfer.d.ts +28 -0
  45. package/columnar/transfer.d.ts.map +1 -0
  46. package/columnar/transfer.js +669 -0
  47. package/columnar/types.d.ts +175 -0
  48. package/columnar/types.d.ts.map +1 -0
  49. package/columnar/types.js +22 -0
  50. package/columnar/worker.d.ts +157 -0
  51. package/columnar/worker.d.ts.map +1 -0
  52. package/columnar/worker.js +1059 -0
  53. package/connect-url-safety.d.ts +53 -0
  54. package/connect-url-safety.js +189 -0
  55. package/contract/pmtiles.d.ts +6 -5
  56. package/contract/pmtiles.d.ts.map +1 -1
  57. package/contract/pmtiles.js +30 -5
  58. package/contract/source.d.ts.map +1 -1
  59. package/contract/source.js +11 -8
  60. package/core/client.d.ts.map +1 -1
  61. package/core/client.js +87 -19
  62. package/core/deprecation-notice.d.ts +28 -0
  63. package/core/deprecation-notice.d.ts.map +1 -0
  64. package/core/deprecation-notice.js +22 -0
  65. package/core/esri-geojson.d.ts.map +1 -1
  66. package/core/esri-geojson.js +14 -8
  67. package/core/optional-grpc-peer.d.ts +15 -0
  68. package/core/optional-grpc-peer.d.ts.map +1 -0
  69. package/core/optional-grpc-peer.js +60 -0
  70. package/esri-compat/basemap-gallery.d.ts +2 -0
  71. package/esri-compat/basemap-gallery.d.ts.map +1 -1
  72. package/esri-compat/basemap-gallery.js +12 -0
  73. package/esri-compat/basemap.d.ts +4 -0
  74. package/esri-compat/basemap.d.ts.map +1 -1
  75. package/esri-compat/basemap.js +70 -0
  76. package/esri-compat/bookmarks.d.ts +2 -0
  77. package/esri-compat/bookmarks.d.ts.map +1 -1
  78. package/esri-compat/bookmarks.js +12 -0
  79. package/esri-compat/controls.d.ts +18 -0
  80. package/esri-compat/controls.d.ts.map +1 -1
  81. package/esri-compat/controls.js +120 -57
  82. package/esri-compat/directions.d.ts +4 -0
  83. package/esri-compat/directions.d.ts.map +1 -1
  84. package/esri-compat/directions.js +30 -20
  85. package/esri-compat/editor.d.ts +4 -0
  86. package/esri-compat/editor.d.ts.map +1 -1
  87. package/esri-compat/editor.js +16 -0
  88. package/esri-compat/esri-config.d.ts +2 -1
  89. package/esri-compat/esri-config.d.ts.map +1 -1
  90. package/esri-compat/feature-effect.d.ts +23 -0
  91. package/esri-compat/feature-effect.d.ts.map +1 -0
  92. package/esri-compat/feature-effect.js +12 -0
  93. package/esri-compat/feature-form.d.ts +6 -0
  94. package/esri-compat/feature-form.d.ts.map +1 -1
  95. package/esri-compat/feature-form.js +63 -0
  96. package/esri-compat/feature-layer.d.ts +21 -2
  97. package/esri-compat/feature-layer.d.ts.map +1 -1
  98. package/esri-compat/feature-layer.js +172 -1
  99. package/esri-compat/feature-table.d.ts +5 -0
  100. package/esri-compat/feature-table.d.ts.map +1 -1
  101. package/esri-compat/feature-table.js +23 -0
  102. package/esri-compat/identity-manager.d.ts +1 -1
  103. package/esri-compat/identity-manager.js +1 -1
  104. package/esri-compat/legend.d.ts +2 -0
  105. package/esri-compat/legend.d.ts.map +1 -1
  106. package/esri-compat/legend.js +98 -0
  107. package/esri-compat/locator.d.ts +1 -1
  108. package/esri-compat/locator.js +1 -1
  109. package/esri-compat/map-view-mount.d.ts +1 -0
  110. package/esri-compat/map-view-mount.d.ts.map +1 -1
  111. package/esri-compat/map-view-mount.js +46 -25
  112. package/esri-compat/map-view.d.ts +13 -0
  113. package/esri-compat/map-view.d.ts.map +1 -1
  114. package/esri-compat/map-view.js +94 -8
  115. package/esri-compat/map.d.ts +5 -1
  116. package/esri-compat/map.d.ts.map +1 -1
  117. package/esri-compat/map.js +10 -0
  118. package/esri-compat/measurement.d.ts +2 -0
  119. package/esri-compat/measurement.d.ts.map +1 -1
  120. package/esri-compat/measurement.js +14 -0
  121. package/esri-compat/point.d.ts +7 -0
  122. package/esri-compat/point.d.ts.map +1 -1
  123. package/esri-compat/point.js +33 -3
  124. package/esri-compat/popup.d.ts +2 -0
  125. package/esri-compat/popup.d.ts.map +1 -1
  126. package/esri-compat/popup.js +12 -0
  127. package/esri-compat/portal.d.ts +1 -1
  128. package/esri-compat/portal.js +1 -1
  129. package/esri-compat/print.d.ts +3 -0
  130. package/esri-compat/print.d.ts.map +1 -1
  131. package/esri-compat/print.js +15 -0
  132. package/esri-compat/search.d.ts +4 -0
  133. package/esri-compat/search.d.ts.map +1 -1
  134. package/esri-compat/search.js +25 -28
  135. package/esri-compat/sketch.d.ts +2 -0
  136. package/esri-compat/sketch.d.ts.map +1 -1
  137. package/esri-compat/sketch.js +12 -0
  138. package/esri-compat/widget-host.d.ts +46 -17
  139. package/esri-compat/widget-host.d.ts.map +1 -1
  140. package/esri-compat/widget-host.js +84 -20
  141. package/esri-compat/widget-shell-hosts.d.ts +61 -0
  142. package/esri-compat/widget-shell-hosts.d.ts.map +1 -0
  143. package/esri-compat/widget-shell-hosts.js +76 -0
  144. package/index.d.ts +3 -1
  145. package/index.js +1 -0
  146. package/offline/digest.d.ts +15 -0
  147. package/offline/digest.js +58 -0
  148. package/offline/quota.d.ts +161 -0
  149. package/offline/quota.js +370 -0
  150. package/offline/types.d.ts +349 -0
  151. package/offline/types.js +75 -0
  152. package/package.json +21 -2
  153. package/query-planner/canonical-filter.d.ts +30 -0
  154. package/query-planner/canonical-filter.d.ts.map +1 -0
  155. package/query-planner/canonical-filter.js +30 -0
  156. package/query-planner/canonical.d.ts +7 -0
  157. package/query-planner/canonical.d.ts.map +1 -0
  158. package/query-planner/canonical.js +145 -0
  159. package/query-planner/columnar-plan.d.ts +63 -0
  160. package/query-planner/columnar-plan.d.ts.map +1 -0
  161. package/query-planner/columnar-plan.js +94 -0
  162. package/query-planner/cql2-json.d.ts +17 -0
  163. package/query-planner/cql2-json.d.ts.map +1 -0
  164. package/query-planner/cql2-json.js +646 -0
  165. package/query-planner/diagnostics.d.ts +35 -0
  166. package/query-planner/diagnostics.d.ts.map +1 -0
  167. package/query-planner/diagnostics.js +386 -0
  168. package/query-planner/duckdb.d.ts +79 -0
  169. package/query-planner/duckdb.d.ts.map +1 -0
  170. package/query-planner/duckdb.js +772 -0
  171. package/query-planner/execution-context.d.ts +5 -0
  172. package/query-planner/execution-context.d.ts.map +1 -0
  173. package/query-planner/execution-context.js +52 -0
  174. package/query-planner/executor.d.ts +5 -0
  175. package/query-planner/executor.d.ts.map +1 -0
  176. package/query-planner/executor.js +229 -0
  177. package/query-planner/geoservices.d.ts +91 -0
  178. package/query-planner/geoservices.d.ts.map +1 -0
  179. package/query-planner/geoservices.js +688 -0
  180. package/query-planner/grpc.d.ts +100 -0
  181. package/query-planner/grpc.d.ts.map +1 -0
  182. package/query-planner/grpc.js +760 -0
  183. package/query-planner/index.d.ts +59 -0
  184. package/query-planner/index.d.ts.map +1 -0
  185. package/query-planner/index.js +43 -0
  186. package/query-planner/ir.d.ts +25 -0
  187. package/query-planner/ir.d.ts.map +1 -0
  188. package/query-planner/ir.js +479 -0
  189. package/query-planner/local-aggregate.d.ts +5 -0
  190. package/query-planner/local-aggregate.d.ts.map +1 -0
  191. package/query-planner/local-aggregate.js +126 -0
  192. package/query-planner/odata-protocol-module.d.ts +54 -0
  193. package/query-planner/odata-protocol-module.d.ts.map +1 -0
  194. package/query-planner/odata-protocol-module.js +199 -0
  195. package/query-planner/odata-v1.d.ts +23 -0
  196. package/query-planner/odata-v1.d.ts.map +1 -0
  197. package/query-planner/odata-v1.js +202 -0
  198. package/query-planner/odata.d.ts +45 -0
  199. package/query-planner/odata.d.ts.map +1 -0
  200. package/query-planner/odata.js +519 -0
  201. package/query-planner/ogc-compiler.d.ts +55 -0
  202. package/query-planner/ogc-compiler.d.ts.map +1 -0
  203. package/query-planner/ogc-compiler.js +480 -0
  204. package/query-planner/ogc-features.d.ts +53 -0
  205. package/query-planner/ogc-features.d.ts.map +1 -0
  206. package/query-planner/ogc-features.js +639 -0
  207. package/query-planner/planner.d.ts +17 -0
  208. package/query-planner/planner.d.ts.map +1 -0
  209. package/query-planner/planner.js +937 -0
  210. package/query-planner/representation.d.ts +59 -0
  211. package/query-planner/representation.d.ts.map +1 -0
  212. package/query-planner/representation.js +140 -0
  213. package/query-planner/resource.d.ts +94 -0
  214. package/query-planner/resource.d.ts.map +1 -0
  215. package/query-planner/resource.js +526 -0
  216. package/query-planner/semantic-canonical.d.ts +15 -0
  217. package/query-planner/semantic-canonical.d.ts.map +1 -0
  218. package/query-planner/semantic-canonical.js +62 -0
  219. package/query-planner/semantic-compat.d.ts +10 -0
  220. package/query-planner/semantic-compat.d.ts.map +1 -0
  221. package/query-planner/semantic-compat.js +35 -0
  222. package/query-planner/semantic-compiler.d.ts +64 -0
  223. package/query-planner/semantic-compiler.d.ts.map +1 -0
  224. package/query-planner/semantic-compiler.js +81 -0
  225. package/query-planner/semantic-literals.d.ts +30 -0
  226. package/query-planner/semantic-literals.d.ts.map +1 -0
  227. package/query-planner/semantic-literals.js +133 -0
  228. package/query-planner/semantic-types.d.ts +265 -0
  229. package/query-planner/semantic-types.d.ts.map +1 -0
  230. package/query-planner/semantic-types.js +1 -0
  231. package/query-planner/semantic.d.ts +54 -0
  232. package/query-planner/semantic.d.ts.map +1 -0
  233. package/query-planner/semantic.js +1421 -0
  234. package/query-planner/types.d.ts +712 -0
  235. package/query-planner/types.d.ts.map +1 -0
  236. package/query-planner/types.js +49 -0
  237. package/query-planner/wfs-protocol-module.d.ts +57 -0
  238. package/query-planner/wfs-protocol-module.d.ts.map +1 -0
  239. package/query-planner/wfs-protocol-module.js +731 -0
  240. package/query-planner/wfs-semantic.d.ts +58 -0
  241. package/query-planner/wfs-semantic.d.ts.map +1 -0
  242. package/query-planner/wfs-semantic.js +547 -0
  243. package/query-planner/wfs-v1.d.ts +41 -0
  244. package/query-planner/wfs-v1.d.ts.map +1 -0
  245. package/query-planner/wfs-v1.js +366 -0
  246. package/query-planner/wfs.d.ts +4 -0
  247. package/query-planner/wfs.d.ts.map +1 -0
  248. package/query-planner/wfs.js +2 -0
  249. package/widget-capabilities.d.ts +67 -0
  250. package/widget-capabilities.js +152 -0
@@ -0,0 +1,772 @@
1
+ /**
2
+ * Compile canonical query IR to deterministic DuckDB SQL over
3
+ * `read_parquet(...)` for the GeoParquet columnar `Source`, without performing
4
+ * any network, filesystem, or DuckDB metadata I/O.
5
+ *
6
+ * The heavy lifting — identifier/literal escaping, spatial predicate pushdown,
7
+ * GeoJSON projection, and GROUP BY aggregation — is delegated to the
8
+ * dependency-free {@link module:core/geoparquet-sql} SQL builder that the live
9
+ * GeoParquet `Source` already uses, so the planner and the adapter emit the
10
+ * exact same SQL for the same inputs.
11
+ *
12
+ * @module
13
+ */
14
+ import { validateExecutableCrsBinding } from "../contract/schema.js";
15
+ import { GeoParquetWhereClauseError, compileAggregate, compileQuery, geometryExpr, parquetSourceExpr, quoteIdentifier, } from "../core/geoparquet-sql.js";
16
+ import { queryFromCanonical } from "./ir.js";
17
+ import { parseGeoParquetResourceHandle } from "./resource.js";
18
+ import { hashSemanticQuery } from "./semantic-canonical.js";
19
+ import { prepareSemanticCompilerQuery, runSemanticCompiler, sameCrsDefinition, sameExecutableCrs, semanticSchemaField, semanticUnsupported, } from "./semantic-compiler.js";
20
+ import { HonuaQueryPlanningError } from "./types.js";
21
+ const RESOURCE_SQL_PLACEHOLDER = "honua-resource://resolve-at-execution";
22
+ /**
23
+ * Compile canonical query IR to a deterministic DuckDB `SELECT`. Reuses the
24
+ * GeoParquet `Source` SQL builder, so the compiled SQL is byte-identical to
25
+ * what the live adapter runs for the same descriptor + query.
26
+ */
27
+ export function compileDuckDbQuery(source, query) {
28
+ if (source.protocol !== "geoparquet") {
29
+ throw new HonuaQueryPlanningError("unsupported-compiler", `duckdb-sql-v1 does not compile protocol "${source.protocol}"`);
30
+ }
31
+ const geoparquet = source.geoparquet;
32
+ if (!geoparquet || geoparquet.sources.length === 0) {
33
+ throw new HonuaQueryPlanningError("invalid-query", `Source "${source.id}" requires locator.url (a parquet file or hive glob) for DuckDB planning`);
34
+ }
35
+ if (query.outSr !== undefined) {
36
+ throw new HonuaQueryPlanningError("unsupported-query", "DuckDB/GeoParquet has no portable output-CRS query option; omit outSr and reproject downstream");
37
+ }
38
+ if (query.spatialFilter && !geoparquet.geometryColumn) {
39
+ throw new HonuaQueryPlanningError("unsupported-query", `Source "${source.id}" has a spatialFilter but no geometry column; set locator.geoparquet.geometryColumn or a geometry schema field`);
40
+ }
41
+ const geometry = geoparquet.geometryColumn
42
+ ? {
43
+ column: geoparquet.geometryColumn,
44
+ encoding: geoparquet.geometryEncoding ?? "wkb",
45
+ ...(geoparquet.bboxColumn ? { bboxColumn: geoparquet.bboxColumn } : {}),
46
+ }
47
+ : undefined;
48
+ const options = {
49
+ sources: geoparquet.sources,
50
+ geometryAlias: "geometry",
51
+ ...(geometry ? { geometry } : {}),
52
+ };
53
+ const request = queryFromCanonical(query);
54
+ try {
55
+ const compiled = query.aggregation
56
+ ? compileAggregate({ ...request, aggregation: query.aggregation }, options)
57
+ : compileQuery(request, options);
58
+ return {
59
+ compiler: "duckdb-sql-v1",
60
+ sql: compiled.sql,
61
+ sources: geoparquet.sources,
62
+ ...(geometry ? { geometryColumn: geometry.column, geometryEncoding: geometry.encoding } : {}),
63
+ ...(compiled.bboxApproximated ? { bboxApproximated: true } : {}),
64
+ };
65
+ }
66
+ catch (error) {
67
+ rethrowRejectedWhere(error);
68
+ throw new HonuaQueryPlanningError("unsupported-query", `DuckDB query cannot be compiled deterministically: ${error instanceof Error ? error.message : String(error)}`);
69
+ }
70
+ }
71
+ /**
72
+ * Surface a contained `Query.where` rejection as an invalid query rather than
73
+ * an unsupported one: the raw-text lane is supported, this specific expression
74
+ * is refused. Only the fixed rejection code travels, never the caller's text.
75
+ */
76
+ function rethrowRejectedWhere(error) {
77
+ if (error instanceof GeoParquetWhereClauseError) {
78
+ throw new HonuaQueryPlanningError("invalid-query", `Query.where was rejected before SQL assembly: ${error.code}`, {
79
+ cause: error,
80
+ });
81
+ }
82
+ }
83
+ /** Compile a credential-free template; execution recompiles with ephemeral resolved sources. */
84
+ export function compileDuckDbQueryV2(source, query) {
85
+ const geoparquet = source.geoparquet;
86
+ if (query.outSr !== undefined) {
87
+ throw new HonuaQueryPlanningError("unsupported-query", "DuckDB/GeoParquet has no portable output-CRS query option; omit outSr and reproject downstream");
88
+ }
89
+ if (query.spatialFilter && !geoparquet.geometryColumn) {
90
+ throw new HonuaQueryPlanningError("unsupported-query", `Source "${source.id}" has a spatialFilter but no geometry column; set locator.geoparquet.geometryColumn or a geometry schema field`);
91
+ }
92
+ const geometry = geoparquet.geometryColumn
93
+ ? {
94
+ column: geoparquet.geometryColumn,
95
+ encoding: geoparquet.geometryEncoding ?? "wkb",
96
+ ...(geoparquet.bboxColumn ? { bboxColumn: geoparquet.bboxColumn } : {}),
97
+ }
98
+ : undefined;
99
+ const options = {
100
+ sources: [RESOURCE_SQL_PLACEHOLDER],
101
+ geometryAlias: "geometry",
102
+ ...(geometry ? { geometry } : {}),
103
+ };
104
+ const request = queryFromCanonical(query);
105
+ try {
106
+ const compiled = query.aggregation
107
+ ? compileAggregate({ ...request, aggregation: query.aggregation }, options)
108
+ : compileQuery(request, options);
109
+ return {
110
+ compiler: "duckdb-sql-v2",
111
+ sqlTemplate: compiled.sql,
112
+ resource: geoparquet.resource,
113
+ ...(geometry ? { geometryColumn: geometry.column, geometryEncoding: geometry.encoding } : {}),
114
+ ...(compiled.bboxApproximated ? { bboxApproximated: true } : {}),
115
+ };
116
+ }
117
+ catch (error) {
118
+ rethrowRejectedWhere(error);
119
+ throw new HonuaQueryPlanningError("unsupported-query", "DuckDB query cannot be compiled safely for an opaque GeoParquet resource");
120
+ }
121
+ }
122
+ /**
123
+ * Compile the schema-validated semantic AST into parameterized DuckDB SQL.
124
+ * The table address remains the fixed opaque-resource placeholder; only a
125
+ * trusted execution boundary may resolve the attached handle to raw locators.
126
+ */
127
+ export function compileSemanticDuckDbQuery(options) {
128
+ return runSemanticCompiler(() => {
129
+ const { query, schema } = prepareSemanticCompilerQuery(options.query, options.schema, "geoparquet");
130
+ verifyDuckDbPhysicalPaths(schema);
131
+ const resource = verifiedSemanticResource(options.resource);
132
+ const geometry = options.geometry ? verifyDuckDbGeometrySource(options.geometry, schema) : undefined;
133
+ const spatialStrategy = verifiedDuckDbSpatialStrategy(options.spatialStrategy);
134
+ const state = {
135
+ schema,
136
+ ...(geometry ? { geometry } : {}),
137
+ spatialStrategy,
138
+ parameters: [],
139
+ spatial: [],
140
+ losses: [],
141
+ usesNativeFilter: false,
142
+ };
143
+ const projection = duckDbProjection(query, state);
144
+ const filter = query.filter
145
+ ? compileDuckDbFilter(query.filter, state, "$.filter")
146
+ : undefined;
147
+ const groupBy = query.kind === "aggregate" ? duckDbGroupBy(query, state) : "";
148
+ const orderBy = duckDbOrderBy(query, state);
149
+ const page = duckDbPage(query);
150
+ const sqlTemplate = `SELECT ${projection.sql} FROM ${parquetSourceExpr([RESOURCE_SQL_PLACEHOLDER])}${filter ? ` WHERE ${filter}` : ""}${groupBy}${orderBy}${page}`;
151
+ const artifact = {
152
+ compiler: "duckdb-semantic-query-v1",
153
+ dialect: "duckdb-sql",
154
+ schemaFingerprint: schema.fingerprint,
155
+ queryFingerprint: hashSemanticQuery(query, { schema, protocol: "geoparquet" }),
156
+ resource,
157
+ sqlTemplate,
158
+ parameters: state.parameters,
159
+ ...(projection.outputGeometry ? { outputGeometry: projection.outputGeometry } : {}),
160
+ spatial: state.spatial,
161
+ usesNativeFilter: state.usesNativeFilter,
162
+ };
163
+ return { artifact, losses: state.losses };
164
+ });
165
+ }
166
+ function verifiedDuckDbSpatialStrategy(value) {
167
+ if (value === undefined || value === "exact")
168
+ return "exact";
169
+ if (value === "bbox-envelope")
170
+ return value;
171
+ throw new HonuaQueryPlanningError("invalid-query", "options.spatialStrategy is invalid");
172
+ }
173
+ function verifiedSemanticResource(value) {
174
+ try {
175
+ return parseGeoParquetResourceHandle(value);
176
+ }
177
+ catch {
178
+ throw new HonuaQueryPlanningError("invalid-query", "Semantic DuckDB compilation requires a valid resource handle");
179
+ }
180
+ }
181
+ function verifyDuckDbGeometrySource(value, schema) {
182
+ if (!value || typeof value !== "object") {
183
+ throw new HonuaQueryPlanningError("invalid-query", "Semantic DuckDB geometry metadata is invalid");
184
+ }
185
+ const field = semanticSchemaField(schema, value.field, "options.geometry.field");
186
+ if (field.type.kind !== "geometry") {
187
+ throw new HonuaQueryPlanningError("invalid-query", "options.geometry.field must identify a geometry field");
188
+ }
189
+ if (field.path.length !== 1) {
190
+ semanticUnsupported("unsupported-geometry", "options.geometry.field", "DuckDB geometry columns must have one physical path segment");
191
+ }
192
+ if (value.encoding !== "wkb" && value.encoding !== "native" && value.encoding !== "geojson") {
193
+ throw new HonuaQueryPlanningError("invalid-query", "options.geometry.encoding is invalid");
194
+ }
195
+ if (value.bboxColumn !== undefined && (typeof value.bboxColumn !== "string" || value.bboxColumn.length === 0)) {
196
+ throw new HonuaQueryPlanningError("invalid-query", "options.geometry.bboxColumn is invalid");
197
+ }
198
+ return {
199
+ field: field.name,
200
+ encoding: value.encoding,
201
+ ...(value.bboxColumn !== undefined ? { bboxColumn: value.bboxColumn } : {}),
202
+ };
203
+ }
204
+ function duckDbProjection(query, state) {
205
+ if (query.kind === "aggregate") {
206
+ if (query.outputCrs) {
207
+ semanticUnsupported("unsupported-projection", "$.outputCrs", "DuckDB aggregate rows do not carry output geometry");
208
+ }
209
+ const parts = query.groupBy.map((name, index) => aliasedDuckDbField(state.schema, name, `$.groupBy[${index}]`));
210
+ query.metrics.forEach((metric, index) => {
211
+ const field = metric.field
212
+ ? semanticSchemaField(state.schema, metric.field, `$.metrics[${index}].field`)
213
+ : undefined;
214
+ const operand = field ? duckDbFieldExpression(field, `$.metrics[${index}].field`) : "*";
215
+ const aggregate = duckDbAggregateFunction(metric.fn, operand);
216
+ parts.push(`${aggregate} AS ${duckDbIdentifier(metric.as, `$.metrics[${index}].as`)}`);
217
+ });
218
+ return { sql: parts.join(", ") };
219
+ }
220
+ const geometryField = selectedDuckDbGeometryField(query, state);
221
+ const names = query.select ?? state.schema.fields.filter((field) => field.type.kind !== "geometry").map((field) => field.name);
222
+ const parts = [];
223
+ let geometryAliasCollisionPath;
224
+ names.forEach((name, index) => {
225
+ const path = `$.select[${index}]`;
226
+ const field = semanticSchemaField(state.schema, name, path);
227
+ if (field.type.kind === "geometry") {
228
+ if (!geometryField || field.name !== geometryField.name) {
229
+ semanticUnsupported("unsupported-projection", path, "DuckDB semantic results expose only the selected top-level geometry");
230
+ }
231
+ return;
232
+ }
233
+ if (duckDbAsciiIdentifierKey(field.name) === "geometry") {
234
+ geometryAliasCollisionPath = query.select ? path : "$.geometry";
235
+ }
236
+ parts.push(aliasedDuckDbField(state.schema, field.name, path));
237
+ });
238
+ if (geometryField) {
239
+ if (geometryAliasCollisionPath) {
240
+ semanticUnsupported("unsupported-projection", geometryAliasCollisionPath, 'DuckDB cannot project an attribute using the reserved geometry result alias "geometry" while returning geometry');
241
+ }
242
+ const geometry = requiredDuckDbGeometry(state, geometryField.name, "$.geometry");
243
+ const geometrySchema = sourceGeometryField(state.schema, geometryField.name, "$.geometry");
244
+ const crs = executableDuckDbCrs(geometrySchema.crs, "$.geometry");
245
+ if (geometrySchema.layout !== "xy" && geometrySchema.layout !== "xyz") {
246
+ semanticUnsupported("unsupported-geometry", "$.geometry", "DuckDB GeoJSON output requires a known xy or xyz source geometry layout");
247
+ }
248
+ assertDuckDbOutputCrs(query, geometryField, state);
249
+ parts.push(`ST_AsGeoJSON(${duckDbGeometryExpression(geometryField, geometry, "$.geometry")}) AS ${quoteIdentifier("geometry")}`);
250
+ return {
251
+ sql: parts.join(", "),
252
+ outputGeometry: {
253
+ sourceField: geometryField.name,
254
+ sourceEncoding: geometry.encoding,
255
+ resultField: "geometry",
256
+ resultEncoding: "geojson",
257
+ crs,
258
+ layout: geometrySchema.layout,
259
+ },
260
+ };
261
+ }
262
+ if (query.outputCrs) {
263
+ semanticUnsupported("unsupported-projection", "$.outputCrs", "DuckDB cannot apply an output CRS when geometry is omitted");
264
+ }
265
+ return { sql: parts.length > 0 ? parts.join(", ") : `1 AS ${quoteIdentifier("__honua_row")}` };
266
+ }
267
+ function selectedDuckDbGeometryField(query, state) {
268
+ if (query.geometry === "omit")
269
+ return undefined;
270
+ if (query.geometry && typeof query.geometry === "object") {
271
+ return semanticSchemaField(state.schema, query.geometry.field, "$.geometry.field");
272
+ }
273
+ const geometry = state.schema.geometry;
274
+ if (geometry.state === "known" && geometry.primaryField.state === "known") {
275
+ return semanticSchemaField(state.schema, geometry.primaryField.field, "$.geometry");
276
+ }
277
+ if (query.geometry === "include") {
278
+ semanticUnsupported("unsupported-projection", "$.geometry", "DuckDB geometry inclusion requires a known primary geometry field");
279
+ }
280
+ return undefined;
281
+ }
282
+ function assertDuckDbOutputCrs(query, field, state) {
283
+ if (!query.outputCrs)
284
+ return;
285
+ const geometry = sourceGeometryField(state.schema, field.name, "$.geometry");
286
+ const sourceCrs = executableDuckDbCrs(geometry.crs, "$.geometry");
287
+ if (!sameCrsDefinition(query.outputCrs, sourceCrs.definition)) {
288
+ semanticUnsupported("crs-transform-required", "$.outputCrs", "DuckDB semantic compilation does not silently insert a CRS transform");
289
+ }
290
+ }
291
+ function duckDbGroupBy(query, state) {
292
+ if (query.groupBy.length === 0)
293
+ return "";
294
+ return ` GROUP BY ${query.groupBy
295
+ .map((name, index) => duckDbFieldExpression(semanticSchemaField(state.schema, name, `$.groupBy[${index}]`), `$.groupBy[${index}]`))
296
+ .join(", ")}`;
297
+ }
298
+ function duckDbAggregateFunction(fn, operand) {
299
+ switch (fn) {
300
+ case "count":
301
+ return `count(${operand})`;
302
+ case "sum":
303
+ return `sum(${operand})`;
304
+ case "avg":
305
+ return `avg(${operand})`;
306
+ case "min":
307
+ return `min(${operand})`;
308
+ case "max":
309
+ return `max(${operand})`;
310
+ case "stddev":
311
+ return `stddev_samp(${operand})`;
312
+ case "variance":
313
+ return `var_samp(${operand})`;
314
+ default:
315
+ semanticUnsupported("unsupported-node", "$.metrics", `DuckDB does not support aggregate ${JSON.stringify(fn)}`);
316
+ }
317
+ }
318
+ function duckDbOrderBy(query, state) {
319
+ if (!query.sort || query.sort.length === 0)
320
+ return "";
321
+ if (query.kind === "aggregate") {
322
+ const groupFields = new Set(query.groupBy);
323
+ const unsupportedIndex = query.sort.findIndex((sort) => !groupFields.has(sort.field));
324
+ if (unsupportedIndex >= 0) {
325
+ semanticUnsupported("unsupported-sort", `$.sort[${unsupportedIndex}].field`, "Aggregate DuckDB queries may sort only by a grouped source field");
326
+ }
327
+ }
328
+ const parts = query.sort.map((sort, index) => {
329
+ const field = semanticSchemaField(state.schema, sort.field, `$.sort[${index}].field`);
330
+ const nulls = sort.nulls === "first" ? " NULLS FIRST" : sort.nulls === "last" ? " NULLS LAST" : "";
331
+ return `${duckDbFieldExpression(field, `$.sort[${index}].field`)} ${sort.direction.toUpperCase()}${nulls}`;
332
+ });
333
+ return ` ORDER BY ${parts.join(", ")}`;
334
+ }
335
+ function duckDbPage(query) {
336
+ if (!query.page)
337
+ return "";
338
+ const limit = query.page.limit === undefined ? "" : ` LIMIT ${query.page.limit}`;
339
+ const offset = query.page.kind === "offset" && query.page.offset > 0 ? ` OFFSET ${query.page.offset}` : "";
340
+ return `${limit}${offset}`;
341
+ }
342
+ function compileDuckDbFilter(filter, state, path) {
343
+ switch (filter.kind) {
344
+ case "comparison": {
345
+ const field = semanticSchemaField(state.schema, filter.left.name, `${path}.left.name`);
346
+ const operator = { eq: "=", ne: "<>", lt: "<", lte: "<=", gt: ">", gte: ">=" }[filter.operator];
347
+ return `${duckDbFieldExpression(field, `${path}.left.name`)} ${operator} ${duckDbLiteralParameter(filter.right.value, field, state, `${path}.right.value`)}`;
348
+ }
349
+ case "list": {
350
+ const field = semanticSchemaField(state.schema, filter.operand.name, `${path}.operand.name`);
351
+ const values = filter.values.map((literal, index) => duckDbLiteralParameter(literal.value, field, state, `${path}.values[${index}].value`));
352
+ return `${duckDbFieldExpression(field, `${path}.operand.name`)} IN (${values.join(", ")})`;
353
+ }
354
+ case "range": {
355
+ const field = semanticSchemaField(state.schema, filter.operand.name, `${path}.operand.name`);
356
+ return `${duckDbFieldExpression(field, `${path}.operand.name`)} BETWEEN ${duckDbLiteralParameter(filter.lower.value, field, state, `${path}.lower.value`)} AND ${duckDbLiteralParameter(filter.upper.value, field, state, `${path}.upper.value`)}`;
357
+ }
358
+ case "null": {
359
+ const field = semanticSchemaField(state.schema, filter.operand.name, `${path}.operand.name`);
360
+ return `${duckDbFieldExpression(field, `${path}.operand.name`)} IS ${filter.operator === "is-not-null" ? "NOT " : ""}NULL`;
361
+ }
362
+ case "pattern": {
363
+ const field = semanticSchemaField(state.schema, filter.operand.name, `${path}.operand.name`);
364
+ const parameter = duckDbParameter(state, filter.pattern, "literal", field);
365
+ return `${duckDbFieldExpression(field, `${path}.operand.name`)} ${filter.caseSensitive === false ? "ILIKE" : "LIKE"} ${parameter}`;
366
+ }
367
+ case "boolean":
368
+ return `(${filter.args
369
+ .map((entry, index) => compileDuckDbFilter(entry, state, `${path}.args[${index}]`))
370
+ .join(` ${filter.operator.toUpperCase()} `)})`;
371
+ case "not":
372
+ return `NOT (${compileDuckDbFilter(filter.arg, state, `${path}.arg`)})`;
373
+ case "temporal": {
374
+ const field = semanticSchemaField(state.schema, filter.operand.name, `${path}.operand.name`);
375
+ if (filter.operator === "before" || filter.operator === "after") {
376
+ const value = filter.value.value;
377
+ return `${duckDbFieldExpression(field, `${path}.operand.name`)} ${filter.operator === "before" ? "<" : ">"} ${duckDbLiteralParameter(value, field, state, `${path}.value.value`)}`;
378
+ }
379
+ const interval = filter.value.value;
380
+ return `${duckDbFieldExpression(field, `${path}.operand.name`)} BETWEEN ${duckDbLiteralParameter(interval[0], field, state, `${path}.value.value[0]`)} AND ${duckDbLiteralParameter(interval[1], field, state, `${path}.value.value[1]`)}`;
381
+ }
382
+ case "spatial":
383
+ return compileDuckDbSpatial(filter, state, path);
384
+ case "native": {
385
+ if (filter.dialect !== "duckdb-sql" || filter.payload.format !== "text") {
386
+ semanticUnsupported("unsupported-native-filter", path, "DuckDB native filters require a duckdb-sql text payload");
387
+ }
388
+ if (hasDuckDbParameterMarker(filter.payload.text)) {
389
+ semanticUnsupported("unsupported-native-filter", `${path}.payload.text`, "DuckDB native filters cannot contain parameter markers because the native payload carries no bind values");
390
+ }
391
+ state.usesNativeFilter = true;
392
+ return `(${filter.payload.text})`;
393
+ }
394
+ }
395
+ }
396
+ function hasDuckDbParameterMarker(text) {
397
+ let index = 0;
398
+ while (index < text.length) {
399
+ const character = text[index];
400
+ if (character === "'" || character === '"') {
401
+ index = skipDuckDbQuotedToken(text, index, character);
402
+ continue;
403
+ }
404
+ if (character === "-" && text[index + 1] === "-") {
405
+ index += 2;
406
+ while (index < text.length && text[index] !== "\n" && text[index] !== "\r")
407
+ index += 1;
408
+ continue;
409
+ }
410
+ if (character === "/" && text[index + 1] === "*") {
411
+ index = skipDuckDbBlockComment(text, index + 2);
412
+ continue;
413
+ }
414
+ if (character === "$") {
415
+ const delimiter = duckDbDollarQuoteDelimiter(text, index);
416
+ if (delimiter) {
417
+ const end = text.indexOf(delimiter, index + delimiter.length);
418
+ index = end === -1 ? text.length : end + delimiter.length;
419
+ continue;
420
+ }
421
+ if (/[0-9A-Za-z_]/.test(text[index + 1] ?? ""))
422
+ return true;
423
+ }
424
+ if (character === "?")
425
+ return true;
426
+ index += 1;
427
+ }
428
+ return false;
429
+ }
430
+ function skipDuckDbQuotedToken(text, start, quote) {
431
+ let index = start + 1;
432
+ while (index < text.length) {
433
+ if (text[index] !== quote) {
434
+ index += 1;
435
+ continue;
436
+ }
437
+ if (text[index + 1] === quote) {
438
+ index += 2;
439
+ continue;
440
+ }
441
+ return index + 1;
442
+ }
443
+ return index;
444
+ }
445
+ function skipDuckDbBlockComment(text, start) {
446
+ let depth = 1;
447
+ let index = start;
448
+ while (index < text.length && depth > 0) {
449
+ if (text[index] === "/" && text[index + 1] === "*") {
450
+ depth += 1;
451
+ index += 2;
452
+ continue;
453
+ }
454
+ if (text[index] === "*" && text[index + 1] === "/") {
455
+ depth -= 1;
456
+ index += 2;
457
+ continue;
458
+ }
459
+ index += 1;
460
+ }
461
+ return index;
462
+ }
463
+ function duckDbDollarQuoteDelimiter(text, start) {
464
+ const match = /^\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/.exec(text.slice(start));
465
+ return match?.[0];
466
+ }
467
+ function verifyDuckDbPhysicalPaths(schema) {
468
+ const paths = new Map();
469
+ schema.fields.forEach((field, index) => {
470
+ const key = JSON.stringify(field.path.map(duckDbAsciiIdentifierKey));
471
+ const previous = paths.get(key);
472
+ if (previous && !sameDuckDbPhysicalPath(previous, field.path)) {
473
+ semanticUnsupported("unsupported-source", `options.schema.fields[${index}].path`, "DuckDB cannot resolve physical field paths that differ only by ASCII case");
474
+ }
475
+ paths.set(key, field.path);
476
+ });
477
+ }
478
+ function sameDuckDbPhysicalPath(left, right) {
479
+ return left.length === right.length && left.every((segment, index) => segment === right[index]);
480
+ }
481
+ function duckDbAsciiIdentifierKey(value) {
482
+ let key = "";
483
+ for (const character of value) {
484
+ const code = character.charCodeAt(0);
485
+ key += code >= 0x41 && code <= 0x5a ? String.fromCharCode(code + 0x20) : character;
486
+ }
487
+ return key;
488
+ }
489
+ function compileDuckDbSpatial(filter, state, path) {
490
+ const fieldName = semanticSpatialField(filter, state.schema, path);
491
+ const field = semanticSchemaField(state.schema, fieldName, `${path}.property.name`);
492
+ const geometry = requiredDuckDbGeometry(state, field.name, path);
493
+ const fieldSchema = sourceGeometryField(state.schema, field.name, path);
494
+ const sourceCrs = executableDuckDbCrs(fieldSchema.crs, `${path}.property`);
495
+ if (fieldSchema.layout !== "xy") {
496
+ semanticUnsupported("unsupported-geometry", path, "DuckDB semantic spatial predicates currently require an xy geometry field");
497
+ }
498
+ const sourceExpression = duckDbGeometryExpression(field, geometry, `${path}.property`);
499
+ if (filter.operator === "bbox-intersects") {
500
+ if (!sameExecutableCrs(filter.bbox.crs, sourceCrs)) {
501
+ semanticUnsupported("crs-transform-required", `${path}.bbox.crs`, "Spatial bbox CRS differs from the source field");
502
+ }
503
+ if (filter.bbox.box.layout !== "xy") {
504
+ semanticUnsupported("unsupported-geometry", `${path}.bbox`, "DuckDB bbox predicates currently require xy bounds");
505
+ }
506
+ const [xmin, ymin, xmax, ymax] = filter.bbox.box.bounds;
507
+ const predicate = duckDbExactEnvelopePredicate(sourceExpression, geometry.bboxColumn, [xmin, ymin, xmax, ymax], state);
508
+ state.spatial.push({ path, field: field.name, encoding: geometry.encoding, strategy: "exact", crs: sourceCrs });
509
+ return predicate;
510
+ }
511
+ if (!sameExecutableCrs(filter.geometry.crs, sourceCrs)) {
512
+ semanticUnsupported("crs-transform-required", `${path}.geometry.crs`, "Spatial geometry CRS differs from the source field");
513
+ }
514
+ if (filter.geometry.layout !== "xy") {
515
+ semanticUnsupported("unsupported-geometry", `${path}.geometry.layout`, "DuckDB semantic spatial predicates currently require xy literal geometry");
516
+ }
517
+ if (state.spatialStrategy === "bbox-envelope" && filter.operator === "intersects") {
518
+ const bounds = executableGeometryBounds(filter.geometry, `${path}.geometry`);
519
+ const predicate = duckDbEnvelopePredicate(sourceExpression, geometry.bboxColumn, bounds, state);
520
+ state.spatial.push({
521
+ path,
522
+ field: field.name,
523
+ encoding: geometry.encoding,
524
+ strategy: "bbox-envelope",
525
+ crs: sourceCrs,
526
+ });
527
+ state.losses.push({
528
+ code: "spatial-envelope-reduction",
529
+ path,
530
+ message: "The intersects operand was explicitly reduced to its bounding envelope",
531
+ });
532
+ return predicate;
533
+ }
534
+ const geometryParameter = duckDbParameter(state, JSON.stringify(filter.geometry.geometry), "geometry", field, "geometry-json");
535
+ const literalExpression = `ST_GeomFromGeoJSON(${geometryParameter})`;
536
+ state.spatial.push({ path, field: field.name, encoding: geometry.encoding, strategy: "exact", crs: sourceCrs });
537
+ if (filter.operator === "within-distance" || filter.operator === "beyond-distance") {
538
+ if (filter.distance.mode !== "planar") {
539
+ semanticUnsupported("unsupported-distance", `${path}.distance.mode`, "DuckDB geometry distance does not preserve geodesic mode");
540
+ }
541
+ assertDuckDbDistanceUnit(filter.distance.unit, sourceCrs, `${path}.distance.unit`);
542
+ const distance = duckDbParameter(state, filter.distance.value, "distance", field, "distance");
543
+ const dWithin = `ST_DWithin(${sourceExpression}, ${literalExpression}, ${distance})`;
544
+ return filter.operator === "within-distance" ? dWithin : `NOT (${dWithin})`;
545
+ }
546
+ const fn = {
547
+ equals: "ST_Equals",
548
+ intersects: "ST_Intersects",
549
+ within: "ST_Within",
550
+ contains: "ST_Contains",
551
+ disjoint: "ST_Disjoint",
552
+ touches: "ST_Touches",
553
+ overlaps: "ST_Overlaps",
554
+ crosses: "ST_Crosses",
555
+ }[filter.operator];
556
+ return `${fn}(${sourceExpression}, ${literalExpression})`;
557
+ }
558
+ function semanticSpatialField(filter, schema, path) {
559
+ if (filter.property)
560
+ return filter.property.name;
561
+ if (schema.geometry.state === "known" && schema.geometry.primaryField.state === "known") {
562
+ return schema.geometry.primaryField.field;
563
+ }
564
+ semanticUnsupported("unsupported-geometry", `${path}.property`, "A spatial predicate requires a geometry field");
565
+ }
566
+ function requiredDuckDbGeometry(state, fieldName, path) {
567
+ if (!state.geometry || state.geometry.field !== fieldName) {
568
+ semanticUnsupported("unsupported-source", path, "DuckDB physical geometry metadata is unavailable for the requested geometry field");
569
+ }
570
+ return state.geometry;
571
+ }
572
+ function sourceGeometryField(schema, fieldName, path) {
573
+ if (schema.geometry.state !== "known") {
574
+ semanticUnsupported("unsupported-source", path, "Source geometry metadata is unavailable");
575
+ }
576
+ const field = schema.geometry.fields.find((candidate) => candidate.field === fieldName);
577
+ if (!field)
578
+ semanticUnsupported("unsupported-source", path, "Source geometry field metadata is unavailable");
579
+ return field;
580
+ }
581
+ function duckDbGeometryExpression(field, geometry, path) {
582
+ try {
583
+ return geometryExpr({ column: field.path[0], encoding: geometry.encoding });
584
+ }
585
+ catch {
586
+ semanticUnsupported("unsupported-source", path, "DuckDB cannot represent the physical geometry field path");
587
+ }
588
+ }
589
+ function duckDbEnvelopePredicate(sourceExpression, bboxColumn, bounds, state) {
590
+ const [xmin, ymin, xmax, ymax] = bounds.map((value) => duckDbParameter(state, value, "bbox", undefined, "number"));
591
+ if (bboxColumn) {
592
+ const bbox = duckDbIdentifier(bboxColumn, "options.geometry.bboxColumn");
593
+ // Keep placeholder appearance in the same xmin/ymin/xmax/ymax order as
594
+ // the parameter array; anonymous DuckDB placeholders cannot be reused.
595
+ return `(${bbox}.xmax >= ${xmin} AND ${bbox}.ymax >= ${ymin} AND ${bbox}.xmin <= ${xmax} AND ${bbox}.ymin <= ${ymax})`;
596
+ }
597
+ return `ST_Intersects(${sourceExpression}, ST_MakeEnvelope(${xmin}, ${ymin}, ${xmax}, ${ymax}))`;
598
+ }
599
+ function duckDbExactEnvelopePredicate(sourceExpression, bboxColumn, bounds, state) {
600
+ if (!bboxColumn)
601
+ return duckDbEnvelopePredicate(sourceExpression, undefined, bounds, state);
602
+ const prefilter = duckDbEnvelopePredicate(sourceExpression, bboxColumn, bounds, state);
603
+ const exact = duckDbEnvelopePredicate(sourceExpression, undefined, bounds, state);
604
+ return `(${prefilter} AND ${exact})`;
605
+ }
606
+ function executableGeometryBounds(value, path) {
607
+ const bounds = {
608
+ xmin: Number.POSITIVE_INFINITY,
609
+ ymin: Number.POSITIVE_INFINITY,
610
+ xmax: Number.NEGATIVE_INFINITY,
611
+ ymax: Number.NEGATIVE_INFINITY,
612
+ seen: false,
613
+ };
614
+ collectGeometryCoordinates(value.geometry, bounds);
615
+ if (!bounds.seen)
616
+ semanticUnsupported("unsupported-geometry", path, "Geometry has no xy coordinates");
617
+ return [bounds.xmin, bounds.ymin, bounds.xmax, bounds.ymax];
618
+ }
619
+ function executableDuckDbCrs(value, path) {
620
+ try {
621
+ return validateExecutableCrsBinding(value, path);
622
+ }
623
+ catch {
624
+ semanticUnsupported("unsupported-crs", path, "DuckDB requires a resolved source CRS and payload order");
625
+ }
626
+ }
627
+ function collectGeometryCoordinates(value, bounds) {
628
+ if (Array.isArray(value)) {
629
+ if (typeof value[0] === "number" && typeof value[1] === "number") {
630
+ bounds.seen = true;
631
+ bounds.xmin = Math.min(bounds.xmin, value[0]);
632
+ bounds.ymin = Math.min(bounds.ymin, value[1]);
633
+ bounds.xmax = Math.max(bounds.xmax, value[0]);
634
+ bounds.ymax = Math.max(bounds.ymax, value[1]);
635
+ return;
636
+ }
637
+ value.forEach((entry) => collectGeometryCoordinates(entry, bounds));
638
+ return;
639
+ }
640
+ if (!value || typeof value !== "object")
641
+ return;
642
+ const record = value;
643
+ if (record.coordinates !== undefined)
644
+ collectGeometryCoordinates(record.coordinates, bounds);
645
+ if (record.geometries !== undefined)
646
+ collectGeometryCoordinates(record.geometries, bounds);
647
+ }
648
+ function assertDuckDbDistanceUnit(unit, crs, path) {
649
+ const axisUnits = crs.coordinateOrder.axes.slice(0, 2).map((axis) => normalizeDistanceUnit(axis.unit));
650
+ const requested = normalizeDistanceUnit(unit);
651
+ if (axisUnits.length < 2 || axisUnits.some((axisUnit) => axisUnit !== requested)) {
652
+ semanticUnsupported("unsupported-distance", path, "DuckDB planar distance unit must match both source coordinate axes");
653
+ }
654
+ }
655
+ function normalizeDistanceUnit(unit) {
656
+ const normalized = unit
657
+ .trim()
658
+ .toLowerCase()
659
+ .replace(/[ _-]+/g, "-");
660
+ if (normalized === "meter" || normalized === "meters")
661
+ return "metre";
662
+ if (normalized === "kilometer" || normalized === "kilometers" || normalized === "kilometres")
663
+ return "kilometre";
664
+ if (normalized === "feet" || normalized === "ft")
665
+ return "foot";
666
+ if (normalized === "us-survey-feet" || normalized === "us-ft")
667
+ return "us-survey-foot";
668
+ if (normalized === "miles" || normalized === "mi")
669
+ return "mile";
670
+ if (normalized === "nautical-miles")
671
+ return "nautical-mile";
672
+ if (normalized === "degrees")
673
+ return "degree";
674
+ if (normalized === "radians")
675
+ return "radian";
676
+ return normalized;
677
+ }
678
+ function duckDbLiteralParameter(value, field, state, path) {
679
+ if (value === null || Array.isArray(value) || typeof value === "object") {
680
+ semanticUnsupported("unsupported-field-type", path, `DuckDB does not bind ${field.type.kind} filter values as scalar parameters`);
681
+ }
682
+ const parameter = duckDbParameter(state, value, "literal", field);
683
+ switch (field.type.kind) {
684
+ case "integer":
685
+ if (field.type.jsonEncoding !== "string")
686
+ return parameter;
687
+ return `CAST(${parameter} AS ${field.type.signed ? "BIGINT" : "UBIGINT"})`;
688
+ case "decimal":
689
+ if (field.type.jsonEncoding !== "string")
690
+ return parameter;
691
+ return `CAST(${parameter} AS ${duckDbDecimalType(field, path)})`;
692
+ case "uuid":
693
+ return `CAST(${parameter} AS UUID)`;
694
+ case "date":
695
+ return `CAST(${parameter} AS DATE)`;
696
+ case "time":
697
+ if (field.type.unit === "nanosecond") {
698
+ semanticUnsupported("unsupported-field-type", path, "DuckDB TIME cannot preserve nanosecond precision");
699
+ }
700
+ return `CAST(${parameter} AS TIME)`;
701
+ case "timestamp":
702
+ return `CAST(${parameter} AS ${duckDbTimestampType(field, path)})`;
703
+ case "duration":
704
+ if (field.type.unit === "nanosecond") {
705
+ semanticUnsupported("unsupported-field-type", path, "DuckDB INTERVAL cannot preserve nanosecond precision");
706
+ }
707
+ return `CAST(${parameter} AS INTERVAL)`;
708
+ case "binary":
709
+ case "json":
710
+ case "geometry":
711
+ case "list":
712
+ case "struct":
713
+ case "union":
714
+ case "unknown":
715
+ return semanticUnsupported("unsupported-field-type", path, `DuckDB semantic filtering does not support logical type ${field.type.kind}`);
716
+ default:
717
+ return parameter;
718
+ }
719
+ }
720
+ function duckDbDecimalType(field, path) {
721
+ if (field.type.kind !== "decimal") {
722
+ semanticUnsupported("unsupported-field-type", path, "DuckDB decimal compilation requires decimal schema metadata");
723
+ }
724
+ if (field.type.precision === undefined || field.type.scale === undefined || field.type.precision > 38) {
725
+ semanticUnsupported("unsupported-field-type", path, "DuckDB string-encoded decimals require precision and scale with precision at most 38");
726
+ }
727
+ return `DECIMAL(${field.type.precision}, ${field.type.scale})`;
728
+ }
729
+ function duckDbTimestampType(field, path) {
730
+ if (field.type.kind !== "timestamp") {
731
+ semanticUnsupported("unsupported-field-type", path, "DuckDB timestamp compilation requires timestamp schema metadata");
732
+ }
733
+ const zoned = field.type.timezone === "utc" || field.type.timezone === "offset";
734
+ if (zoned) {
735
+ if (field.type.unit === "nanosecond") {
736
+ semanticUnsupported("unsupported-field-type", path, "DuckDB TIMESTAMPTZ cannot preserve nanosecond precision");
737
+ }
738
+ return "TIMESTAMPTZ";
739
+ }
740
+ return {
741
+ second: "TIMESTAMP_S",
742
+ millisecond: "TIMESTAMP_MS",
743
+ microsecond: "TIMESTAMP",
744
+ nanosecond: "TIMESTAMP_NS",
745
+ }[field.type.unit];
746
+ }
747
+ function duckDbParameter(state, value, role, field, logicalType) {
748
+ state.parameters.push({
749
+ position: state.parameters.length + 1,
750
+ role,
751
+ value,
752
+ ...(field ? { field: field.name } : {}),
753
+ ...(logicalType || field ? { logicalType: logicalType ?? field?.type.kind } : {}),
754
+ });
755
+ return "?";
756
+ }
757
+ function duckDbFieldExpression(field, path = "$.schema.fields") {
758
+ return field.path.map((segment) => duckDbIdentifier(segment, path)).join(".");
759
+ }
760
+ function duckDbIdentifier(value, path) {
761
+ try {
762
+ return quoteIdentifier(value);
763
+ }
764
+ catch {
765
+ semanticUnsupported("unsupported-source", path, "DuckDB cannot represent the physical identifier");
766
+ }
767
+ }
768
+ function aliasedDuckDbField(schema, name, path) {
769
+ const field = semanticSchemaField(schema, name, path);
770
+ const expression = duckDbFieldExpression(field, path);
771
+ return `${expression} AS ${duckDbIdentifier(field.name, path)}`;
772
+ }