@honua/sdk-esri-compat 0.1.12 → 0.1.13

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 (248) 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/core/client.d.ts.map +1 -1
  59. package/core/client.js +87 -19
  60. package/core/deprecation-notice.d.ts +28 -0
  61. package/core/deprecation-notice.d.ts.map +1 -0
  62. package/core/deprecation-notice.js +22 -0
  63. package/core/esri-geojson.d.ts.map +1 -1
  64. package/core/esri-geojson.js +10 -6
  65. package/core/optional-grpc-peer.d.ts +15 -0
  66. package/core/optional-grpc-peer.d.ts.map +1 -0
  67. package/core/optional-grpc-peer.js +60 -0
  68. package/esri-compat/basemap-gallery.d.ts +2 -0
  69. package/esri-compat/basemap-gallery.d.ts.map +1 -1
  70. package/esri-compat/basemap-gallery.js +12 -0
  71. package/esri-compat/basemap.d.ts +4 -0
  72. package/esri-compat/basemap.d.ts.map +1 -1
  73. package/esri-compat/basemap.js +70 -0
  74. package/esri-compat/bookmarks.d.ts +2 -0
  75. package/esri-compat/bookmarks.d.ts.map +1 -1
  76. package/esri-compat/bookmarks.js +12 -0
  77. package/esri-compat/controls.d.ts +18 -0
  78. package/esri-compat/controls.d.ts.map +1 -1
  79. package/esri-compat/controls.js +120 -57
  80. package/esri-compat/directions.d.ts +4 -0
  81. package/esri-compat/directions.d.ts.map +1 -1
  82. package/esri-compat/directions.js +30 -20
  83. package/esri-compat/editor.d.ts +4 -0
  84. package/esri-compat/editor.d.ts.map +1 -1
  85. package/esri-compat/editor.js +16 -0
  86. package/esri-compat/esri-config.d.ts +2 -1
  87. package/esri-compat/esri-config.d.ts.map +1 -1
  88. package/esri-compat/feature-effect.d.ts +23 -0
  89. package/esri-compat/feature-effect.d.ts.map +1 -0
  90. package/esri-compat/feature-effect.js +12 -0
  91. package/esri-compat/feature-form.d.ts +6 -0
  92. package/esri-compat/feature-form.d.ts.map +1 -1
  93. package/esri-compat/feature-form.js +63 -0
  94. package/esri-compat/feature-layer.d.ts +21 -2
  95. package/esri-compat/feature-layer.d.ts.map +1 -1
  96. package/esri-compat/feature-layer.js +172 -1
  97. package/esri-compat/feature-table.d.ts +5 -0
  98. package/esri-compat/feature-table.d.ts.map +1 -1
  99. package/esri-compat/feature-table.js +23 -0
  100. package/esri-compat/identity-manager.d.ts +1 -1
  101. package/esri-compat/identity-manager.js +1 -1
  102. package/esri-compat/legend.d.ts +2 -0
  103. package/esri-compat/legend.d.ts.map +1 -1
  104. package/esri-compat/legend.js +98 -0
  105. package/esri-compat/locator.d.ts +1 -1
  106. package/esri-compat/locator.js +1 -1
  107. package/esri-compat/map-view-mount.d.ts +1 -0
  108. package/esri-compat/map-view-mount.d.ts.map +1 -1
  109. package/esri-compat/map-view-mount.js +46 -25
  110. package/esri-compat/map-view.d.ts +13 -0
  111. package/esri-compat/map-view.d.ts.map +1 -1
  112. package/esri-compat/map-view.js +94 -8
  113. package/esri-compat/map.d.ts +5 -1
  114. package/esri-compat/map.d.ts.map +1 -1
  115. package/esri-compat/map.js +10 -0
  116. package/esri-compat/measurement.d.ts +2 -0
  117. package/esri-compat/measurement.d.ts.map +1 -1
  118. package/esri-compat/measurement.js +14 -0
  119. package/esri-compat/point.d.ts +7 -0
  120. package/esri-compat/point.d.ts.map +1 -1
  121. package/esri-compat/point.js +33 -3
  122. package/esri-compat/popup.d.ts +2 -0
  123. package/esri-compat/popup.d.ts.map +1 -1
  124. package/esri-compat/popup.js +12 -0
  125. package/esri-compat/portal.d.ts +1 -1
  126. package/esri-compat/portal.js +1 -1
  127. package/esri-compat/print.d.ts +3 -0
  128. package/esri-compat/print.d.ts.map +1 -1
  129. package/esri-compat/print.js +15 -0
  130. package/esri-compat/search.d.ts +4 -0
  131. package/esri-compat/search.d.ts.map +1 -1
  132. package/esri-compat/search.js +25 -28
  133. package/esri-compat/sketch.d.ts +2 -0
  134. package/esri-compat/sketch.d.ts.map +1 -1
  135. package/esri-compat/sketch.js +12 -0
  136. package/esri-compat/widget-host.d.ts +46 -17
  137. package/esri-compat/widget-host.d.ts.map +1 -1
  138. package/esri-compat/widget-host.js +84 -20
  139. package/esri-compat/widget-shell-hosts.d.ts +61 -0
  140. package/esri-compat/widget-shell-hosts.d.ts.map +1 -0
  141. package/esri-compat/widget-shell-hosts.js +76 -0
  142. package/index.d.ts +3 -1
  143. package/index.js +1 -0
  144. package/offline/digest.d.ts +15 -0
  145. package/offline/digest.js +58 -0
  146. package/offline/quota.d.ts +161 -0
  147. package/offline/quota.js +370 -0
  148. package/offline/types.d.ts +349 -0
  149. package/offline/types.js +75 -0
  150. package/package.json +21 -2
  151. package/query-planner/canonical-filter.d.ts +30 -0
  152. package/query-planner/canonical-filter.d.ts.map +1 -0
  153. package/query-planner/canonical-filter.js +30 -0
  154. package/query-planner/canonical.d.ts +7 -0
  155. package/query-planner/canonical.d.ts.map +1 -0
  156. package/query-planner/canonical.js +145 -0
  157. package/query-planner/columnar-plan.d.ts +63 -0
  158. package/query-planner/columnar-plan.d.ts.map +1 -0
  159. package/query-planner/columnar-plan.js +94 -0
  160. package/query-planner/cql2-json.d.ts +17 -0
  161. package/query-planner/cql2-json.d.ts.map +1 -0
  162. package/query-planner/cql2-json.js +646 -0
  163. package/query-planner/diagnostics.d.ts +35 -0
  164. package/query-planner/diagnostics.d.ts.map +1 -0
  165. package/query-planner/diagnostics.js +386 -0
  166. package/query-planner/duckdb.d.ts +79 -0
  167. package/query-planner/duckdb.d.ts.map +1 -0
  168. package/query-planner/duckdb.js +772 -0
  169. package/query-planner/execution-context.d.ts +5 -0
  170. package/query-planner/execution-context.d.ts.map +1 -0
  171. package/query-planner/execution-context.js +52 -0
  172. package/query-planner/executor.d.ts +5 -0
  173. package/query-planner/executor.d.ts.map +1 -0
  174. package/query-planner/executor.js +229 -0
  175. package/query-planner/geoservices.d.ts +91 -0
  176. package/query-planner/geoservices.d.ts.map +1 -0
  177. package/query-planner/geoservices.js +688 -0
  178. package/query-planner/grpc.d.ts +100 -0
  179. package/query-planner/grpc.d.ts.map +1 -0
  180. package/query-planner/grpc.js +760 -0
  181. package/query-planner/index.d.ts +59 -0
  182. package/query-planner/index.d.ts.map +1 -0
  183. package/query-planner/index.js +43 -0
  184. package/query-planner/ir.d.ts +25 -0
  185. package/query-planner/ir.d.ts.map +1 -0
  186. package/query-planner/ir.js +479 -0
  187. package/query-planner/local-aggregate.d.ts +5 -0
  188. package/query-planner/local-aggregate.d.ts.map +1 -0
  189. package/query-planner/local-aggregate.js +126 -0
  190. package/query-planner/odata-protocol-module.d.ts +54 -0
  191. package/query-planner/odata-protocol-module.d.ts.map +1 -0
  192. package/query-planner/odata-protocol-module.js +199 -0
  193. package/query-planner/odata-v1.d.ts +23 -0
  194. package/query-planner/odata-v1.d.ts.map +1 -0
  195. package/query-planner/odata-v1.js +202 -0
  196. package/query-planner/odata.d.ts +45 -0
  197. package/query-planner/odata.d.ts.map +1 -0
  198. package/query-planner/odata.js +519 -0
  199. package/query-planner/ogc-compiler.d.ts +55 -0
  200. package/query-planner/ogc-compiler.d.ts.map +1 -0
  201. package/query-planner/ogc-compiler.js +480 -0
  202. package/query-planner/ogc-features.d.ts +53 -0
  203. package/query-planner/ogc-features.d.ts.map +1 -0
  204. package/query-planner/ogc-features.js +639 -0
  205. package/query-planner/planner.d.ts +17 -0
  206. package/query-planner/planner.d.ts.map +1 -0
  207. package/query-planner/planner.js +937 -0
  208. package/query-planner/representation.d.ts +59 -0
  209. package/query-planner/representation.d.ts.map +1 -0
  210. package/query-planner/representation.js +140 -0
  211. package/query-planner/resource.d.ts +94 -0
  212. package/query-planner/resource.d.ts.map +1 -0
  213. package/query-planner/resource.js +526 -0
  214. package/query-planner/semantic-canonical.d.ts +15 -0
  215. package/query-planner/semantic-canonical.d.ts.map +1 -0
  216. package/query-planner/semantic-canonical.js +62 -0
  217. package/query-planner/semantic-compat.d.ts +10 -0
  218. package/query-planner/semantic-compat.d.ts.map +1 -0
  219. package/query-planner/semantic-compat.js +35 -0
  220. package/query-planner/semantic-compiler.d.ts +64 -0
  221. package/query-planner/semantic-compiler.d.ts.map +1 -0
  222. package/query-planner/semantic-compiler.js +81 -0
  223. package/query-planner/semantic-literals.d.ts +30 -0
  224. package/query-planner/semantic-literals.d.ts.map +1 -0
  225. package/query-planner/semantic-literals.js +133 -0
  226. package/query-planner/semantic-types.d.ts +265 -0
  227. package/query-planner/semantic-types.d.ts.map +1 -0
  228. package/query-planner/semantic-types.js +1 -0
  229. package/query-planner/semantic.d.ts +54 -0
  230. package/query-planner/semantic.d.ts.map +1 -0
  231. package/query-planner/semantic.js +1421 -0
  232. package/query-planner/types.d.ts +712 -0
  233. package/query-planner/types.d.ts.map +1 -0
  234. package/query-planner/types.js +49 -0
  235. package/query-planner/wfs-protocol-module.d.ts +57 -0
  236. package/query-planner/wfs-protocol-module.d.ts.map +1 -0
  237. package/query-planner/wfs-protocol-module.js +731 -0
  238. package/query-planner/wfs-semantic.d.ts +58 -0
  239. package/query-planner/wfs-semantic.d.ts.map +1 -0
  240. package/query-planner/wfs-semantic.js +547 -0
  241. package/query-planner/wfs-v1.d.ts +41 -0
  242. package/query-planner/wfs-v1.d.ts.map +1 -0
  243. package/query-planner/wfs-v1.js +366 -0
  244. package/query-planner/wfs.d.ts +4 -0
  245. package/query-planner/wfs.d.ts.map +1 -0
  246. package/query-planner/wfs.js +2 -0
  247. package/widget-capabilities.d.ts +67 -0
  248. package/widget-capabilities.js +152 -0
@@ -0,0 +1,937 @@
1
+ import { CAPABILITIES, capabilities, } from "../contract/types.js";
2
+ import { canonicalStringify, sha256, toJsonValue } from "./canonical.js";
3
+ import { createCacheDecision, createPlanValidity, createPlanWarnings, createQueryPlanProvenance, createStepDiagnostics, summarizeFidelity, summarizePlanBounds, } from "./diagnostics.js";
4
+ import { compileDuckDbQuery, compileDuckDbQueryV2 } from "./duckdb.js";
5
+ import { compileGeoServicesQuery } from "./geoservices.js";
6
+ import { compileGrpcQuery } from "./grpc.js";
7
+ import { createGeoParquetQueryIr, createQueryIr, deepFreeze, queryFromCanonical } from "./ir.js";
8
+ import { compileOdataQuery, odataProtocolQueryCompiler } from "./odata-v1.js";
9
+ import { compileOgcApiFeaturesQuery } from "./ogc-features.js";
10
+ import { createQueryPlanRepresentationDecision, normalizeRepresentationRequest } from "./representation.js";
11
+ import { parseGeoParquetResourceHandle } from "./resource.js";
12
+ import { HonuaQueryPlanExecutionError, HonuaQueryPlanningError, MAX_LOCAL_MATERIALIZATION_ROWS, QUERY_PLAN_DIAGNOSTICS_VERSION, QUERY_PLAN_KIND, QUERY_PLAN_V2_VERSION, QUERY_PLAN_VERSION, } from "./types.js";
13
+ import { compileWfsQuery, wfsProtocolQueryCompiler } from "./wfs-v1.js";
14
+ export function explainQuery(options) {
15
+ const resource = ownGeoParquetResource(options);
16
+ if (!resource && options.descriptor.protocol === "geoparquet")
17
+ assertCredentialFreeLegacyDescriptor(options.descriptor);
18
+ const ir = resource ? createGeoParquetQueryIr({ ...options, geoparquetResource: resource }) : createQueryIr(options);
19
+ const capabilityPolicy = options.capabilityPolicy ?? "strict";
20
+ const fallback = normalizeFallback(options.fallback);
21
+ const estimates = normalizeEstimates(options.estimates);
22
+ const executionMode = normalizeExecutionMode(options.executionMode);
23
+ // Representation is chosen before any step is compiled so an unsatisfiable
24
+ // explicit pin fails closed before the plan takes shape.
25
+ const representation = createQueryPlanRepresentationDecision(ir, estimates, normalizeRepresentationRequest(options.representation));
26
+ let baseSteps;
27
+ let pushdown;
28
+ if (ir.query.aggregation && !ir.source.capabilities.includes("queryAggregate")) {
29
+ if (capabilityPolicy !== "degraded" || fallback.mode === "disabled") {
30
+ throw new HonuaQueryPlanningError(capabilityPolicy === "degraded" ? "fallback-disabled" : "capability-not-supported", `Source "${ir.source.id}" does not support queryAggregate; select degraded policy with an explicit bounded-local fallback to proceed`);
31
+ }
32
+ if (!ir.source.capabilities.includes("query")) {
33
+ throw new HonuaQueryPlanningError("capability-not-supported", `Source "${ir.source.id}" cannot run the bounded fallback because it does not support query`);
34
+ }
35
+ rejectUnsafeEstimate(fallback, estimates.rows, estimates.bytes);
36
+ const ogcFallback = ir.source.protocol === "ogc-features";
37
+ const adapterOwnsLookahead = ogcFallback || ir.source.protocol === "odata" || ir.source.protocol === "wfs";
38
+ const inputQuery = localAggregateInputQuery(ir.query, ir.query.aggregation, fallback.maxRows, options.descriptor.schema?.primaryKey, { preserveGeometry: ogcFallback, adapterOwnsLookahead });
39
+ baseSteps = [
40
+ {
41
+ id: "remote-input",
42
+ engine: "remote",
43
+ operation: "queryAll",
44
+ pushdown: "partial",
45
+ reason: `Push filters and projection to ${remoteEngineName(ir.source.protocol)}, fetching at most ${fallback.maxRows + 1} rows as an overflow sentinel.`,
46
+ requests: estimates.requests ?? 1,
47
+ query: inputQuery,
48
+ compiled: compileRemoteQuery(ir.source, inputQuery, "queryAll"),
49
+ },
50
+ {
51
+ id: "local-aggregate",
52
+ engine: "client",
53
+ operation: "aggregate",
54
+ pushdown: "none",
55
+ reason: `queryAggregate is unavailable; compute only after enforcing the ${fallback.maxRows}-row local ceiling.`,
56
+ inputStepId: "remote-input",
57
+ aggregation: ir.query.aggregation,
58
+ maxRows: fallback.maxRows,
59
+ ...(fallback.maxBytes !== undefined ? { maxBytes: fallback.maxBytes } : {}),
60
+ },
61
+ ];
62
+ pushdown = "partial";
63
+ }
64
+ else {
65
+ const capability = ir.query.aggregation ? "queryAggregate" : "query";
66
+ if (!ir.source.capabilities.includes(capability)) {
67
+ throw new HonuaQueryPlanningError("capability-not-supported", `Source "${ir.source.id}" does not support ${capability}`);
68
+ }
69
+ if (estimates.requests !== undefined && estimates.requests !== 1) {
70
+ throw new HonuaQueryPlanningError("invalid-query", `estimates.requests must be 1 for the exact single-request ${capability} operation`);
71
+ }
72
+ const spatialAggregation = ir.query.aggregation !== undefined && ir.query.spatialFilter !== undefined;
73
+ baseSteps = [
74
+ {
75
+ id: "remote-query",
76
+ engine: "remote",
77
+ operation: ir.query.aggregation ? "queryAggregate" : "query",
78
+ pushdown: "full",
79
+ reason: spatialAggregation
80
+ ? `${remoteEngineName(ir.source.protocol)} executes the spatial aggregation server-side: the spatial filter constrains the grouped statistics without materializing features.`
81
+ : `${remoteEngineName(ir.source.protocol)} can execute the complete canonical query remotely.`,
82
+ requests: 1,
83
+ query: ir.query,
84
+ compiled: compileRemoteQuery(ir.source, ir.query),
85
+ },
86
+ ];
87
+ pushdown = "full";
88
+ }
89
+ const provenance = createQueryPlanProvenance(options.descriptor, ir.source, options);
90
+ const steps = baseSteps.map((step) => ({ ...step, ...createStepDiagnostics(step, estimates, provenance) }));
91
+ const { fidelity, losses } = summarizeFidelity(steps);
92
+ const bounds = summarizePlanBounds(steps);
93
+ const cache = createCacheDecision(options.cache);
94
+ const validity = createPlanValidity(ir, provenance, capabilityPolicy, fallback, executionMode, representation.selected);
95
+ const warnings = createPlanWarnings(baseSteps, ir.source, representation);
96
+ const unsigned = {
97
+ kind: QUERY_PLAN_KIND,
98
+ version: ir.version === "2.0" ? QUERY_PLAN_V2_VERSION : QUERY_PLAN_VERSION,
99
+ ir,
100
+ capabilityPolicy,
101
+ fallback,
102
+ pushdown,
103
+ diagnosticsVersion: QUERY_PLAN_DIAGNOSTICS_VERSION,
104
+ fidelity,
105
+ losses,
106
+ bounds,
107
+ cache,
108
+ provenance,
109
+ validity,
110
+ representation,
111
+ estimates,
112
+ steps,
113
+ warnings,
114
+ };
115
+ const fingerprint = sha256(canonicalStringify(toJsonValue(unsigned)));
116
+ return deepFreeze({
117
+ ...unsigned,
118
+ id: `plan_${fingerprint.slice("sha256:".length, "sha256:".length + 16)}`,
119
+ fingerprint,
120
+ });
121
+ }
122
+ function compileRemoteQuery(source, query, operation = "query") {
123
+ switch (source.protocol) {
124
+ case "geoservices-feature-service":
125
+ return compileGeoServicesQuery(source, query);
126
+ case "ogc-features":
127
+ return ogcQueryAllWireRequest(compileOgcApiFeaturesQuery(source, query), operation);
128
+ case "wfs":
129
+ return wfsProtocolQueryCompiler.compile({ source, query, operation });
130
+ case "odata":
131
+ return odataProtocolQueryCompiler.compile({ source, query, operation });
132
+ case "geoparquet":
133
+ return isGeoParquetV2Source(source)
134
+ ? compileDuckDbQueryV2(source, query)
135
+ : compileDuckDbQuery(source, query);
136
+ case "grpc":
137
+ return compileGrpcQuery(source, query);
138
+ default:
139
+ throw new HonuaQueryPlanningError("unsupported-compiler", `No deterministic query compiler is registered for protocol "${source.protocol}"`);
140
+ }
141
+ }
142
+ function isGeoParquetV2Source(source) {
143
+ return source.protocol === "geoparquet" && source.geoparquet !== undefined && "resource" in source.geoparquet;
144
+ }
145
+ function ogcQueryAllWireRequest(compiled, operation) {
146
+ if (operation !== "queryAll" || compiled.limit === undefined)
147
+ return compiled;
148
+ return deepFreeze({ ...compiled, limit: compiled.limit + 1 });
149
+ }
150
+ function remoteEngineName(protocol) {
151
+ switch (protocol) {
152
+ case "ogc-features":
153
+ return "OGC API Features";
154
+ case "geoservices-feature-service":
155
+ return "GeoServices";
156
+ case "wfs":
157
+ return "WFS 2.0";
158
+ case "odata":
159
+ return "OData v4";
160
+ case "geoparquet":
161
+ return "DuckDB SQL (columnar)";
162
+ case "grpc":
163
+ return "Honua gRPC FeatureService";
164
+ default:
165
+ return protocol;
166
+ }
167
+ }
168
+ export function hashQueryPlan(plan) {
169
+ return persistedPlanSnapshot(plan).hash;
170
+ }
171
+ /** @internal Lightweight v1 integrity path for stable consumers that cannot execute v2 resources. */
172
+ export function hashQueryPlanV1(plan) {
173
+ try {
174
+ const detached = toJsonValue(plan);
175
+ if (!isPlanEnvelope(detached) || detached.version !== QUERY_PLAN_VERSION || detached.ir.version !== "1.0") {
176
+ return undefined;
177
+ }
178
+ if (detached.ir.source.protocol === "geoparquet") {
179
+ const sources = detached.ir.source.geoparquet?.sources;
180
+ if (!sources || sources.length === 0 || sources.some((source) => !isCredentialFreeLegacySource(source))) {
181
+ return undefined;
182
+ }
183
+ }
184
+ if (containsCredentialBearingPlanMaterial(detached))
185
+ return undefined;
186
+ const { id: _id, fingerprint: _fingerprint, ...unsigned } = detached;
187
+ return sha256(canonicalStringify(toJsonValue(unsigned)));
188
+ }
189
+ catch {
190
+ return undefined;
191
+ }
192
+ }
193
+ /** Internal executor boundary: validate once and continue with an immutable detached snapshot. */
194
+ export function validateQueryPlanSnapshot(plan) {
195
+ const snapshot = persistedPlanSnapshot(plan);
196
+ if (snapshot.hash !== snapshot.plan.fingerprint)
197
+ throw invalidPersistedPlan();
198
+ return snapshot.plan;
199
+ }
200
+ /** Canonical persistence boundary; validated legacy protocol artifacts are upgraded before serialization. */
201
+ export function serializeQueryPlan(plan) {
202
+ const snapshot = persistedPlanSnapshot(plan);
203
+ if (snapshot.hash !== snapshot.plan.fingerprint)
204
+ throw invalidPersistedPlan();
205
+ return canonicalStringify(toJsonValue(snapshot.plan));
206
+ }
207
+ /** Parse a persisted plan, upgrading validated legacy protocol artifacts without retaining unsafe input. */
208
+ export function parseQueryPlan(serialized) {
209
+ if (typeof serialized !== "string")
210
+ throw invalidPersistedPlan();
211
+ let value;
212
+ try {
213
+ value = JSON.parse(serialized);
214
+ }
215
+ catch {
216
+ throw invalidPersistedPlan();
217
+ }
218
+ const snapshot = persistedPlanSnapshot(value);
219
+ if (snapshot.hash !== snapshot.plan.fingerprint)
220
+ throw invalidPersistedPlan();
221
+ return snapshot.plan;
222
+ }
223
+ /** Stable cache identity derived only after the plan passes the persistence guard. */
224
+ export function queryPlanCacheKey(plan) {
225
+ const snapshot = persistedPlanSnapshot(plan);
226
+ if (snapshot.hash !== snapshot.plan.fingerprint)
227
+ throw invalidPersistedPlan();
228
+ return `honua-query-plan:${snapshot.plan.version}:${snapshot.plan.fingerprint}`;
229
+ }
230
+ /** Explicitly migrate a credential-free v1 GeoParquet plan to opaque v2 identity. */
231
+ export function migrateGeoParquetQueryPlanV1(legacyPlan, resourceValue) {
232
+ const parsed = parseQueryPlan(serializeQueryPlan(legacyPlan));
233
+ if (parsed.version !== QUERY_PLAN_VERSION || parsed.ir.source.protocol !== "geoparquet") {
234
+ throw invalidPersistedPlan();
235
+ }
236
+ const resource = parseGeoParquetResourceHandle(resourceValue);
237
+ const legacyGeoParquet = parsed.ir.source.geoparquet;
238
+ if (!legacyGeoParquet)
239
+ throw invalidPersistedPlan();
240
+ const descriptor = {
241
+ id: resource.resource.id,
242
+ protocol: "geoparquet",
243
+ locator: {
244
+ url: "honua-resource://opaque",
245
+ geoparquet: {
246
+ ...(legacyGeoParquet.geometryColumn ? { geometryColumn: legacyGeoParquet.geometryColumn } : {}),
247
+ ...(legacyGeoParquet.geometryEncoding ? { geometryEncoding: legacyGeoParquet.geometryEncoding } : {}),
248
+ ...(legacyGeoParquet.bboxColumn ? { bboxColumn: legacyGeoParquet.bboxColumn } : {}),
249
+ ...(legacyGeoParquet.nativeDimensions ? { nativeDimensions: legacyGeoParquet.nativeDimensions } : {}),
250
+ },
251
+ },
252
+ capabilities: capabilities(parsed.ir.source.capabilities),
253
+ };
254
+ return explainQuery({
255
+ descriptor,
256
+ query: queryFromCanonical(parsed.ir.query),
257
+ geoparquetResource: resource,
258
+ capabilityPolicy: parsed.capabilityPolicy,
259
+ fallback: parsed.fallback,
260
+ ...(parsed.ir.source.schemaVersion !== undefined ? { schemaVersion: parsed.ir.source.schemaVersion } : {}),
261
+ ...(parsed.ir.source.sourceVersion !== undefined ? { sourceVersion: parsed.ir.source.sourceVersion } : {}),
262
+ authorizationScope: parsed.ir.source.authorizationScope,
263
+ estimates: parsed.estimates,
264
+ representation: normalizeRepresentationRequest(persistedRepresentationRequest(parsed)),
265
+ });
266
+ }
267
+ function ownGeoParquetResource(options) {
268
+ let descriptor;
269
+ try {
270
+ descriptor = Object.getOwnPropertyDescriptor(options, "geoparquetResource");
271
+ }
272
+ catch {
273
+ throw new HonuaQueryPlanningError("invalid-query", "GeoParquet resource handle is invalid");
274
+ }
275
+ if (!descriptor)
276
+ return undefined;
277
+ if (!("value" in descriptor)) {
278
+ throw new HonuaQueryPlanningError("invalid-query", "GeoParquet resource handle is invalid");
279
+ }
280
+ try {
281
+ return parseGeoParquetResourceHandle(descriptor.value);
282
+ }
283
+ catch {
284
+ throw new HonuaQueryPlanningError("invalid-query", "GeoParquet resource handle is invalid");
285
+ }
286
+ }
287
+ function assertCredentialFreeLegacyDescriptor(descriptor) {
288
+ const sources = [descriptor.locator.url, ...(descriptor.locator.geoparquet?.urls ?? [])];
289
+ if (sources.length === 0 || sources.some((source) => !isCredentialFreeLegacySource(source))) {
290
+ throw new HonuaQueryPlanningError("invalid-query", "Credential-bearing GeoParquet locators require an opaque resource handle");
291
+ }
292
+ }
293
+ function assertPlanPersistenceSafe(plan) {
294
+ assertStructuredDiagnostics(plan);
295
+ if (plan.version === QUERY_PLAN_VERSION) {
296
+ if (plan.ir.version !== "1.0")
297
+ throw invalidPersistedPlan();
298
+ if (plan.ir.source.protocol === "geoparquet") {
299
+ const sources = plan.ir.source.geoparquet?.sources;
300
+ if (!sources || sources.length === 0 || sources.some((source) => !isCredentialFreeLegacySource(source))) {
301
+ throw invalidPersistedPlan();
302
+ }
303
+ }
304
+ const expected = isLegacyOdataQueryPlanV1(plan)
305
+ ? rebuildLegacyOdataQueryPlanV1(plan)
306
+ : isLegacyWfsQueryPlanV1(plan)
307
+ ? rebuildLegacyWfsQueryPlanV1(plan)
308
+ : rebuildQueryPlanV1(plan);
309
+ if (canonicalStringify(toJsonValue(plan)) !== canonicalStringify(toJsonValue(expected))) {
310
+ throw invalidPersistedPlan();
311
+ }
312
+ return;
313
+ }
314
+ if (plan.version !== QUERY_PLAN_V2_VERSION ||
315
+ plan.ir.version !== "2.0" ||
316
+ plan.ir.source.protocol !== "geoparquet" ||
317
+ plan.ir.source.endpoint !== "[opaque-resource]") {
318
+ throw invalidPersistedPlan();
319
+ }
320
+ const expected = rebuildGeoParquetQueryPlanV2(plan);
321
+ if (canonicalStringify(toJsonValue(plan)) !== canonicalStringify(toJsonValue(expected)))
322
+ throw invalidPersistedPlan();
323
+ }
324
+ function assertStructuredDiagnostics(plan) {
325
+ if (plan.capabilityPolicy !== "strict" && plan.capabilityPolicy !== "degraded")
326
+ throw invalidPersistedPlan();
327
+ const fallback = normalizeFallback(plan.fallback);
328
+ const estimates = normalizeEstimates(plan.estimates);
329
+ const executionMode = normalizeExecutionMode(plan.validity.executionMode);
330
+ // Re-derive the representation decision from the persisted request plus the
331
+ // persisted IR/estimates, so a tampered `selected`, `available`, or `reason`
332
+ // cannot survive the canonical comparison below.
333
+ const representation = createQueryPlanRepresentationDecision(plan.ir, estimates, normalizeRepresentationRequest(persistedRepresentationRequest(plan)));
334
+ if (!sameCanonicalValue(plan.fallback, fallback) || !sameCanonicalValue(plan.estimates, estimates)) {
335
+ throw invalidPersistedPlan();
336
+ }
337
+ const expectedProvenance = rebuildPersistedProvenance(plan);
338
+ if (!sameCanonicalValue(plan.provenance, expectedProvenance))
339
+ throw invalidPersistedPlan();
340
+ const expectedSteps = plan.steps.map((step) => ({
341
+ pushdown: persistedStepPushdown(step),
342
+ ...createStepDiagnostics(step, estimates, expectedProvenance),
343
+ }));
344
+ for (let index = 0; index < plan.steps.length; index += 1) {
345
+ const step = plan.steps[index];
346
+ const expected = expectedSteps[index];
347
+ if (!step || !expected)
348
+ throw invalidPersistedPlan();
349
+ const actual = {
350
+ pushdown: step.pushdown,
351
+ fidelity: step.fidelity,
352
+ losses: step.losses,
353
+ bounds: step.bounds,
354
+ provenance: step.provenance,
355
+ };
356
+ if (!sameCanonicalValue(actual, expected))
357
+ throw invalidPersistedPlan();
358
+ if (step.engine === "remote") {
359
+ const expectedRequests = step.operation === "queryAll" ? (estimates.requests ?? 1) : 1;
360
+ if (step.requests !== expectedRequests)
361
+ throw invalidPersistedPlan();
362
+ }
363
+ }
364
+ const expectedFidelity = summarizeFidelity(expectedSteps);
365
+ const expected = {
366
+ diagnosticsVersion: QUERY_PLAN_DIAGNOSTICS_VERSION,
367
+ pushdown: summarizePersistedPushdown(expectedSteps),
368
+ fidelity: expectedFidelity.fidelity,
369
+ losses: expectedFidelity.losses,
370
+ bounds: summarizePlanBounds(expectedSteps),
371
+ cache: createCacheDecision(persistedCacheOptions(plan.cache)),
372
+ provenance: expectedProvenance,
373
+ validity: createPlanValidity(plan.ir, expectedProvenance, plan.capabilityPolicy, fallback, executionMode, representation.selected),
374
+ representation,
375
+ warnings: createPlanWarnings(plan.steps, plan.ir.source, representation),
376
+ };
377
+ const actual = {
378
+ diagnosticsVersion: plan.diagnosticsVersion,
379
+ pushdown: plan.pushdown,
380
+ fidelity: plan.fidelity,
381
+ losses: plan.losses,
382
+ bounds: plan.bounds,
383
+ cache: plan.cache,
384
+ provenance: plan.provenance,
385
+ validity: plan.validity,
386
+ representation: plan.representation,
387
+ warnings: plan.warnings,
388
+ };
389
+ if (!sameCanonicalValue(actual, expected))
390
+ throw invalidPersistedPlan();
391
+ }
392
+ /** Read the persisted representation request without trusting any other member of the decision. */
393
+ function persistedRepresentationRequest(plan) {
394
+ const decision = plan.representation;
395
+ if (decision === null || typeof decision !== "object" || Array.isArray(decision))
396
+ throw invalidPersistedPlan();
397
+ return decision.requested;
398
+ }
399
+ function persistedStepPushdown(step) {
400
+ if (step.engine === "client") {
401
+ if (step.operation !== "aggregate")
402
+ throw invalidPersistedPlan();
403
+ return "none";
404
+ }
405
+ if (step.engine !== "remote")
406
+ throw invalidPersistedPlan();
407
+ if (step.operation === "queryAll")
408
+ return "partial";
409
+ if (step.operation === "query" || step.operation === "queryAggregate")
410
+ return "full";
411
+ throw invalidPersistedPlan();
412
+ }
413
+ function summarizePersistedPushdown(steps) {
414
+ return steps.every((step) => step.pushdown === "full") ? "full" : "partial";
415
+ }
416
+ function rebuildPersistedProvenance(plan) {
417
+ const source = plan.ir.source;
418
+ const schema = plan.provenance.schema;
419
+ const descriptor = {
420
+ id: source.id,
421
+ protocol: source.protocol,
422
+ locator: { url: source.endpoint },
423
+ capabilities: capabilities(parsePlanCapabilities(source.capabilities)),
424
+ ...(schema.state === "known" && schema.basis === "schema-v2"
425
+ ? { schemaV2: { kind: "honua.source-schema", version: "2.0", fingerprint: schema.fingerprint } }
426
+ : {}),
427
+ };
428
+ return createQueryPlanProvenance(descriptor, source, {
429
+ ...(source.schemaVersion ? { schemaVersion: source.schemaVersion } : {}),
430
+ discovery: persistedDiscoveryContext(plan.provenance.discovery),
431
+ });
432
+ }
433
+ /** Rebuild legacy plans through the same pure planner to bind every persisted step to the IR. */
434
+ function rebuildQueryPlanV1(plan, options = {}) {
435
+ const source = plan.ir.source;
436
+ const sourceAuthorityFingerprint = parseSourceAuthorityFingerprint(source.authorityFingerprint);
437
+ const capabilitiesValue = parsePlanCapabilities(source.capabilities);
438
+ const geometryProperty = parseOptionalPlanMetadata(source.geometryProperty);
439
+ const primaryKey = persistedV1FallbackPrimaryKey(plan);
440
+ const schema = geometryProperty || primaryKey
441
+ ? {
442
+ ...(geometryProperty ? { fields: [{ name: geometryProperty, type: "esriFieldTypeGeometry" }] } : {}),
443
+ ...(primaryKey ? { primaryKey } : {}),
444
+ }
445
+ : undefined;
446
+ const geoparquet = source.geoparquet;
447
+ const url = source.protocol === "geoparquet" ? geoparquet?.sources[0] : source.endpoint;
448
+ if (typeof url !== "string" || (source.protocol === "geoparquet" && url.length === 0)) {
449
+ throw invalidPersistedPlan();
450
+ }
451
+ const geometryEncoding = parsePersistedGeoParquetGeometryEncoding(geoparquet?.geometryEncoding);
452
+ const nativeDimensions = parsePersistedNativeDimensions(geoparquet?.nativeDimensions);
453
+ const locator = {
454
+ url,
455
+ ...(source.serviceId !== undefined ? { serviceId: parseOptionalPlanMetadata(source.serviceId) } : {}),
456
+ ...(source.layerId !== undefined ? { layerId: parseOptionalLocatorNumber(source.layerId) } : {}),
457
+ ...(source.collectionId !== undefined ? { collectionId: parseOptionalLocatorId(source.collectionId) } : {}),
458
+ ...(source.typeName !== undefined ? { typeName: parseOptionalPlanMetadata(source.typeName) } : {}),
459
+ ...(source.entitySet !== undefined ? { entitySet: parseOptionalPlanMetadata(source.entitySet) } : {}),
460
+ ...(source.srsName !== undefined ? { srsName: parseOptionalPlanMetadata(source.srsName) } : {}),
461
+ ...(geoparquet
462
+ ? {
463
+ geoparquet: {
464
+ ...(geoparquet.sources.length > 1 ? { urls: geoparquet.sources.slice(1) } : {}),
465
+ ...(geoparquet.geometryColumn
466
+ ? { geometryColumn: parseOptionalPlanMetadata(geoparquet.geometryColumn) }
467
+ : {}),
468
+ ...(geometryEncoding ? { geometryEncoding } : {}),
469
+ ...(geoparquet.bboxColumn ? { bboxColumn: parseOptionalPlanMetadata(geoparquet.bboxColumn) } : {}),
470
+ ...(nativeDimensions ? { nativeDimensions } : {}),
471
+ },
472
+ }
473
+ : {}),
474
+ };
475
+ const descriptor = {
476
+ id: requirePlanMetadata(source.id),
477
+ protocol: source.protocol,
478
+ locator,
479
+ capabilities: capabilities(capabilitiesValue),
480
+ ...(schema ? { schema } : {}),
481
+ ...(plan.provenance.schema.state === "known" && plan.provenance.schema.basis === "schema-v2"
482
+ ? {
483
+ schemaV2: {
484
+ kind: "honua.source-schema",
485
+ version: "2.0",
486
+ fingerprint: plan.provenance.schema.fingerprint,
487
+ },
488
+ }
489
+ : {}),
490
+ };
491
+ return explainQuery({
492
+ descriptor,
493
+ query: queryFromCanonical(plan.ir.query),
494
+ capabilityPolicy: plan.capabilityPolicy,
495
+ fallback: plan.fallback,
496
+ ...(source.schemaVersion !== undefined ? { schemaVersion: source.schemaVersion } : {}),
497
+ ...(source.sourceVersion !== undefined ? { sourceVersion: source.sourceVersion } : {}),
498
+ authorizationScope: source.authorizationScope,
499
+ ...(sourceAuthorityFingerprint
500
+ ? { sourceAuthorityFingerprint }
501
+ : options.legacyWfsAuthority
502
+ ? { sourceAuthorityFingerprint: null }
503
+ : {}),
504
+ estimates: plan.estimates,
505
+ cache: persistedCacheOptions(plan.cache),
506
+ discovery: persistedDiscoveryContext(plan.provenance.discovery),
507
+ executionMode: plan.validity.executionMode,
508
+ representation: normalizeRepresentationRequest(persistedRepresentationRequest(plan)),
509
+ });
510
+ }
511
+ function parseSourceAuthorityFingerprint(value) {
512
+ if (value === undefined)
513
+ return undefined;
514
+ if (typeof value !== "string" || !/^sha256:[0-9a-f]{64}$/.test(value)) {
515
+ throw invalidPersistedPlan();
516
+ }
517
+ return value;
518
+ }
519
+ /**
520
+ * Rebuild the exact OData plan representation emitted before operation-bound
521
+ * protocol artifacts were introduced. The persisted plan version remains
522
+ * 1.0, so those integrity-checked snapshots must be validated against their
523
+ * original compiler semantics before they can be migrated.
524
+ */
525
+ function rebuildLegacyOdataQueryPlanV1(plan) {
526
+ return rebuildLegacyProtocolQueryPlanV1(plan, "odata", compileOdataQuery);
527
+ }
528
+ function isLegacyOdataQueryPlanV1(plan) {
529
+ if (plan.version !== QUERY_PLAN_VERSION || plan.ir.version !== "1.0" || plan.ir.source.protocol !== "odata") {
530
+ return false;
531
+ }
532
+ const remoteSteps = plan.steps.filter((step) => step.engine === "remote");
533
+ return remoteSteps.length > 0 && remoteSteps.every((step) => step.compiled.compiler === "odata-v4-query-v1");
534
+ }
535
+ /**
536
+ * Rebuild the exact WFS plan representation emitted before operation-bound
537
+ * protocol artifacts were introduced. This validates the old artifact against
538
+ * the old compiler before the same-version plan is migrated.
539
+ */
540
+ function rebuildLegacyWfsQueryPlanV1(plan) {
541
+ return rebuildLegacyProtocolQueryPlanV1(plan, "wfs", compileWfsQuery);
542
+ }
543
+ function isLegacyWfsQueryPlanV1(plan) {
544
+ if (plan.version !== QUERY_PLAN_VERSION || plan.ir.version !== "1.0" || plan.ir.source.protocol !== "wfs") {
545
+ return false;
546
+ }
547
+ const remoteSteps = plan.steps.filter((step) => step.engine === "remote");
548
+ return remoteSteps.length > 0 && remoteSteps.every((step) => step.compiled.compiler === "wfs-2.0-get-feature-v1");
549
+ }
550
+ function rebuildLegacyProtocolQueryPlanV1(plan, protocol, compile) {
551
+ const modern = rebuildQueryPlanV1(plan, {
552
+ legacyWfsAuthority: protocol === "wfs" && plan.ir.source.authorityFingerprint === undefined,
553
+ });
554
+ if (modern.ir.source.protocol !== protocol)
555
+ throw invalidPersistedPlan();
556
+ const baseSteps = modern.steps.map((step) => {
557
+ const { fidelity: _fidelity, losses: _losses, bounds: _bounds, provenance: _provenance, ...base } = step;
558
+ if (base.engine !== "remote")
559
+ return base;
560
+ const query = base.operation === "queryAll" ? legacyProtocolQueryAllInput(base.query, modern.fallback) : base.query;
561
+ return {
562
+ ...base,
563
+ query,
564
+ compiled: compile(modern.ir.source, query),
565
+ };
566
+ });
567
+ const steps = baseSteps.map((step) => {
568
+ const diagnostics = createStepDiagnostics(step, modern.estimates, modern.provenance);
569
+ return step.engine === "client" ? { ...step, ...diagnostics, fidelity: "equivalent" } : { ...step, ...diagnostics };
570
+ });
571
+ const { fidelity, losses } = summarizeFidelity(steps);
572
+ const bounds = summarizePlanBounds(steps);
573
+ const warnings = createPlanWarnings(baseSteps, modern.ir.source, modern.representation);
574
+ const { id: _id, fingerprint: _fingerprint, fidelity: _modernFidelity, losses: _modernLosses, bounds: _modernBounds, steps: _modernSteps, warnings: _modernWarnings, ...shared } = modern;
575
+ const unsigned = {
576
+ ...shared,
577
+ fidelity,
578
+ losses,
579
+ bounds,
580
+ steps,
581
+ warnings,
582
+ };
583
+ const fingerprint = sha256(canonicalStringify(toJsonValue(unsigned)));
584
+ return deepFreeze({
585
+ ...unsigned,
586
+ id: `plan_${fingerprint.slice("sha256:".length, "sha256:".length + 16)}`,
587
+ fingerprint,
588
+ });
589
+ }
590
+ function legacyProtocolQueryAllInput(query, fallback) {
591
+ if (fallback.mode !== "bounded-local")
592
+ throw invalidPersistedPlan();
593
+ const pagination = query.pagination;
594
+ if (!pagination ||
595
+ pagination.offset !== 0 ||
596
+ pagination.limit !== fallback.maxRows ||
597
+ fallback.maxRows >= Number.MAX_SAFE_INTEGER) {
598
+ throw invalidPersistedPlan();
599
+ }
600
+ return deepFreeze({
601
+ ...query,
602
+ pagination: { offset: 0, limit: fallback.maxRows + 1 },
603
+ });
604
+ }
605
+ function persistedV1FallbackPrimaryKey(plan) {
606
+ const aggregation = plan.ir.query.aggregation;
607
+ if (!aggregation)
608
+ return undefined;
609
+ const requiresProjectedField = (aggregation.groupBy?.length ?? 0) > 0 || aggregation.metrics.some((metric) => metric.field !== "*");
610
+ if (requiresProjectedField)
611
+ return undefined;
612
+ const remote = plan.steps[0];
613
+ if (remote?.engine !== "remote" || remote.operation !== "queryAll")
614
+ return undefined;
615
+ const outFields = remote.query.outFields;
616
+ if (outFields === undefined)
617
+ return undefined;
618
+ if (outFields.length !== 1)
619
+ throw invalidPersistedPlan();
620
+ return parseOptionalPlanMetadata(outFields[0]);
621
+ }
622
+ function parseOptionalLocatorNumber(value) {
623
+ if (!Number.isSafeInteger(value) || value < 0)
624
+ throw invalidPersistedPlan();
625
+ return value;
626
+ }
627
+ function parseOptionalLocatorId(value) {
628
+ if (typeof value === "number")
629
+ return parseOptionalLocatorNumber(value);
630
+ return requirePlanMetadata(value);
631
+ }
632
+ function requirePlanMetadata(value) {
633
+ const parsed = parseOptionalPlanMetadata(value);
634
+ if (parsed === undefined)
635
+ throw invalidPersistedPlan();
636
+ return parsed;
637
+ }
638
+ function sameCanonicalValue(left, right) {
639
+ return canonicalStringify(toJsonValue(left)) === canonicalStringify(toJsonValue(right));
640
+ }
641
+ /**
642
+ * Rebuild a v2 plan through the pure planner so persistence accepts only one
643
+ * complete canonical projection. This performs no resolution or network I/O.
644
+ */
645
+ function rebuildGeoParquetQueryPlanV2(plan) {
646
+ const source = plan.ir.source;
647
+ const resource = parseGeoParquetResourceHandle(source.geoparquet.resource);
648
+ const sourceCapabilities = parsePlanCapabilities(source.capabilities);
649
+ const primaryKey = parseOptionalPlanMetadata(source.primaryKey);
650
+ const schemaVersion = parseOptionalPlanMetadata(source.schemaVersion);
651
+ const sourceVersion = parseOptionalPlanMetadata(source.sourceVersion);
652
+ const geometryColumn = parseOptionalPlanMetadata(source.geoparquet.geometryColumn);
653
+ const bboxColumn = parseOptionalPlanMetadata(source.geoparquet.bboxColumn);
654
+ const geometryEncoding = parsePersistedGeoParquetGeometryEncoding(source.geoparquet.geometryEncoding);
655
+ const nativeDimensions = parsePersistedNativeDimensions(source.geoparquet.nativeDimensions);
656
+ if (plan.capabilityPolicy !== "strict" && plan.capabilityPolicy !== "degraded")
657
+ throw invalidPersistedPlan();
658
+ const descriptor = {
659
+ id: resource.resource.id,
660
+ protocol: "geoparquet",
661
+ locator: {
662
+ url: "honua-resource://opaque",
663
+ geoparquet: {
664
+ ...(geometryColumn ? { geometryColumn } : {}),
665
+ ...(geometryEncoding ? { geometryEncoding } : {}),
666
+ ...(bboxColumn ? { bboxColumn } : {}),
667
+ ...(nativeDimensions ? { nativeDimensions } : {}),
668
+ },
669
+ },
670
+ capabilities: capabilities(sourceCapabilities),
671
+ ...(primaryKey ? { schema: { primaryKey } } : {}),
672
+ ...(plan.provenance.schema.state === "known" && plan.provenance.schema.basis === "schema-v2"
673
+ ? {
674
+ schemaV2: {
675
+ kind: "honua.source-schema",
676
+ version: "2.0",
677
+ fingerprint: plan.provenance.schema.fingerprint,
678
+ },
679
+ }
680
+ : {}),
681
+ };
682
+ return explainQuery({
683
+ descriptor,
684
+ geoparquetResource: resource,
685
+ query: queryFromCanonical(plan.ir.query),
686
+ capabilityPolicy: plan.capabilityPolicy,
687
+ fallback: plan.fallback,
688
+ ...(schemaVersion ? { schemaVersion } : {}),
689
+ ...(sourceVersion ? { sourceVersion } : {}),
690
+ ...(source.geoparquet.effectiveSchema ? { effectiveSchema: source.geoparquet.effectiveSchema } : {}),
691
+ authorizationScope: source.authorizationScope,
692
+ estimates: plan.estimates,
693
+ cache: persistedCacheOptions(plan.cache),
694
+ discovery: persistedDiscoveryContext(plan.provenance.discovery),
695
+ executionMode: plan.validity.executionMode,
696
+ representation: normalizeRepresentationRequest(persistedRepresentationRequest(plan)),
697
+ });
698
+ }
699
+ function parsePersistedGeoParquetGeometryEncoding(value) {
700
+ if (value === undefined)
701
+ return undefined;
702
+ switch (value) {
703
+ case "wkb":
704
+ case "native":
705
+ case "geojson":
706
+ case "geoarrow-point":
707
+ case "geoarrow-linestring":
708
+ case "geoarrow-polygon":
709
+ case "geoarrow-multipoint":
710
+ case "geoarrow-multilinestring":
711
+ case "geoarrow-multipolygon":
712
+ return value;
713
+ default:
714
+ throw invalidPersistedPlan();
715
+ }
716
+ }
717
+ function parsePersistedNativeDimensions(value) {
718
+ if (value === undefined)
719
+ return undefined;
720
+ if (value !== "xy" && value !== "xyz")
721
+ throw invalidPersistedPlan();
722
+ return value;
723
+ }
724
+ function persistedCacheOptions(cache) {
725
+ return {
726
+ policy: cache.policy,
727
+ freshness: cache.freshness,
728
+ ...(cache.validator ? { validator: { kind: cache.validator.kind, fingerprint: cache.validator.fingerprint } } : {}),
729
+ };
730
+ }
731
+ function persistedDiscoveryContext(discovery) {
732
+ return {
733
+ state: discovery.state,
734
+ ...(discovery.sourceFingerprint ? { sourceFingerprint: discovery.sourceFingerprint } : {}),
735
+ ...(discovery.validator
736
+ ? { validator: { kind: discovery.validator.kind, fingerprint: discovery.validator.fingerprint } }
737
+ : {}),
738
+ };
739
+ }
740
+ function parsePlanCapabilities(value) {
741
+ if (!Array.isArray(value) ||
742
+ value.some((capability) => typeof capability !== "string" || !CAPABILITIES.includes(capability))) {
743
+ throw invalidPersistedPlan();
744
+ }
745
+ return value;
746
+ }
747
+ function parseOptionalPlanMetadata(value) {
748
+ if (value === undefined)
749
+ return undefined;
750
+ if (typeof value !== "string" || value.length === 0 || value.length > 1_024 || containsControlCharacter(value)) {
751
+ throw invalidPersistedPlan();
752
+ }
753
+ return value;
754
+ }
755
+ const CREDENTIAL_BEARING_PLAN_TEXT = /(?:\bBearer\s+[A-Za-z0-9._~+/=-]{8,}|\bBasic\s+[A-Za-z0-9+/=]{8,}|\bAKIA[0-9A-Z]{16}\b|[?&;](?:access[-_]?token|id[-_]?token|refresh[-_]?token|x-amz-signature|x-goog-credential|signature|sig|token|api[-_]?key|password|secret)=[^\s&#;]*|\b(?:authorization|proxy[-_]?authorization|x-api-key|password|secret|token|api[-_]?key|account[-_]?key|shared[-_]?access[-_]?signature)\s*(?:=|:)\s*[^\s,;]+|[a-z][a-z0-9+.-]*:\/\/[^/\s"'<>]*@|\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b)/i;
756
+ const CREDENTIAL_BEARING_PLAN_KEYS = new Set([
757
+ "authorization",
758
+ "proxyauthorization",
759
+ "cookie",
760
+ "setcookie",
761
+ "username",
762
+ "password",
763
+ "passwd",
764
+ "secret",
765
+ "token",
766
+ "accesstoken",
767
+ "refreshtoken",
768
+ "idtoken",
769
+ "apikey",
770
+ "xapikey",
771
+ "accesskey",
772
+ "secretkey",
773
+ "clientsecret",
774
+ "credential",
775
+ "credentials",
776
+ "signature",
777
+ "xamzsignature",
778
+ "xgoogcredential",
779
+ "accountkey",
780
+ "sharedaccesssignature",
781
+ ]);
782
+ /** Detach once so hostile accessors/proxies cannot change between validation and hashing. */
783
+ function persistedPlanSnapshot(value) {
784
+ try {
785
+ const plan = deepFreeze(toJsonValue(value));
786
+ if (!isPlanEnvelope(plan))
787
+ throw invalidPersistedPlan();
788
+ assertPlanPersistenceSafe(plan);
789
+ if (containsCredentialBearingPlanMaterial(plan))
790
+ throw invalidPersistedPlan();
791
+ const acceptedPlan = isLegacyOdataQueryPlanV1(plan) || isLegacyWfsQueryPlanV1(plan) ? rebuildQueryPlanV1(plan) : plan;
792
+ const { id: _id, fingerprint: _fingerprint, ...unsigned } = acceptedPlan;
793
+ return { plan: acceptedPlan, hash: sha256(canonicalStringify(toJsonValue(unsigned))) };
794
+ }
795
+ catch {
796
+ throw invalidPersistedPlan();
797
+ }
798
+ }
799
+ function containsCredentialBearingPlanMaterial(value, path = []) {
800
+ if (typeof value === "string")
801
+ return CREDENTIAL_BEARING_PLAN_TEXT.test(value);
802
+ if (Array.isArray(value)) {
803
+ return value.some((child, index) => containsCredentialBearingPlanMaterial(child, [...path, String(index)]));
804
+ }
805
+ if (value === null || typeof value !== "object")
806
+ return false;
807
+ return Object.entries(value).some(([key, child]) => {
808
+ const normalizedKey = key.replace(/[^A-Za-z0-9]/g, "").toLowerCase();
809
+ const parent = path
810
+ .at(-1)
811
+ ?.replace(/[^A-Za-z0-9]/g, "")
812
+ .toLowerCase();
813
+ const sensitiveHeader = parent === "headers" &&
814
+ (normalizedKey === "xauth" ||
815
+ normalizedKey === "xauthtoken" ||
816
+ normalizedKey === "xaccesskey" ||
817
+ normalizedKey === "xsessiontoken");
818
+ const sensitiveLocator = parent === "locator" && (normalizedKey === "user" || normalizedKey === "userinfo");
819
+ const sensitiveQueryParameter = parent === "query" && (normalizedKey === "auth" || normalizedKey === "key" || normalizedKey === "session");
820
+ return (CREDENTIAL_BEARING_PLAN_KEYS.has(normalizedKey) ||
821
+ sensitiveHeader ||
822
+ sensitiveLocator ||
823
+ sensitiveQueryParameter ||
824
+ containsCredentialBearingPlanMaterial(child, [...path, key]));
825
+ });
826
+ }
827
+ function isCredentialFreeLegacySource(value) {
828
+ if (typeof value !== "string" || value.length === 0 || value.trim() !== value || containsControlCharacter(value)) {
829
+ return false;
830
+ }
831
+ try {
832
+ const url = new URL(value);
833
+ return !url.username && !url.password && !url.search && !url.hash;
834
+ }
835
+ catch {
836
+ return !value.includes("?") && !value.includes("#") && !value.includes("@");
837
+ }
838
+ }
839
+ function containsControlCharacter(value) {
840
+ for (let index = 0; index < value.length; index += 1) {
841
+ const code = value.charCodeAt(index);
842
+ if (code <= 0x1f || code === 0x7f)
843
+ return true;
844
+ }
845
+ return false;
846
+ }
847
+ function isPlanEnvelope(value) {
848
+ if (value === null || typeof value !== "object" || Array.isArray(value))
849
+ return false;
850
+ const plan = value;
851
+ return (plan.kind === QUERY_PLAN_KIND &&
852
+ (plan.version === QUERY_PLAN_VERSION || plan.version === QUERY_PLAN_V2_VERSION) &&
853
+ plan.diagnosticsVersion === QUERY_PLAN_DIAGNOSTICS_VERSION &&
854
+ typeof plan.id === "string" &&
855
+ typeof plan.fingerprint === "string" &&
856
+ /^sha256:[0-9a-f]{64}$/.test(plan.fingerprint) &&
857
+ plan.ir !== null &&
858
+ typeof plan.ir === "object" &&
859
+ Array.isArray(plan.steps) &&
860
+ Array.isArray(plan.warnings));
861
+ }
862
+ function normalizeExecutionMode(value) {
863
+ if (value === undefined)
864
+ return "snapshot";
865
+ if (value !== "snapshot" && value !== "delta") {
866
+ throw new HonuaQueryPlanningError("invalid-query", "executionMode is invalid");
867
+ }
868
+ return value;
869
+ }
870
+ function invalidPersistedPlan() {
871
+ return new HonuaQueryPlanExecutionError("invalid-plan", "Query plan is invalid or unsafe to persist");
872
+ }
873
+ function normalizeFallback(fallback) {
874
+ if (!fallback || fallback.mode === "disabled")
875
+ return deepFreeze({ mode: "disabled" });
876
+ if (!Number.isSafeInteger(fallback.maxRows) || fallback.maxRows < 1) {
877
+ throw new HonuaQueryPlanningError("unsafe-materialization", "bounded-local maxRows must be a positive safe integer");
878
+ }
879
+ if (fallback.maxRows > MAX_LOCAL_MATERIALIZATION_ROWS) {
880
+ throw new HonuaQueryPlanningError("unsafe-materialization", `bounded-local maxRows ${fallback.maxRows} exceeds the SDK safety ceiling ${MAX_LOCAL_MATERIALIZATION_ROWS}`);
881
+ }
882
+ if (fallback.maxBytes !== undefined && (!Number.isSafeInteger(fallback.maxBytes) || fallback.maxBytes < 1)) {
883
+ throw new HonuaQueryPlanningError("unsafe-materialization", "bounded-local maxBytes must be a positive safe integer");
884
+ }
885
+ return deepFreeze({
886
+ mode: "bounded-local",
887
+ maxRows: fallback.maxRows,
888
+ ...(fallback.maxBytes !== undefined ? { maxBytes: fallback.maxBytes } : {}),
889
+ });
890
+ }
891
+ function normalizeEstimates(estimates) {
892
+ const out = {};
893
+ for (const key of ["rows", "bytes", "requests"]) {
894
+ const value = estimates?.[key];
895
+ if (value === undefined)
896
+ continue;
897
+ if (key === "requests" && (!Number.isSafeInteger(value) || value < 1)) {
898
+ throw new HonuaQueryPlanningError("invalid-query", "estimates.requests must be a safe integer >= 1");
899
+ }
900
+ if (!Number.isFinite(value) || value < 0) {
901
+ throw new HonuaQueryPlanningError("invalid-query", `estimates.${key} must be a finite non-negative number`);
902
+ }
903
+ out[key] = value;
904
+ }
905
+ return deepFreeze(out);
906
+ }
907
+ function rejectUnsafeEstimate(fallback, rows, bytes) {
908
+ if (rows !== undefined && rows > fallback.maxRows) {
909
+ throw new HonuaQueryPlanningError("unsafe-materialization", `Estimated input ${rows} rows exceeds bounded-local maxRows ${fallback.maxRows}`);
910
+ }
911
+ if (fallback.maxBytes !== undefined && bytes !== undefined && bytes > fallback.maxBytes) {
912
+ throw new HonuaQueryPlanningError("unsafe-materialization", `Estimated input ${bytes} bytes exceeds bounded-local maxBytes ${fallback.maxBytes}`);
913
+ }
914
+ }
915
+ function localAggregateInputQuery(query, aggregation, maxRows, primaryKey, options = {}) {
916
+ if (aggregation.histogram || aggregation.timeSeries) {
917
+ throw new HonuaQueryPlanningError("unsupported-query", "The first bounded-local compiler supports metrics and groupBy; histogram/timeSeries remain follow-on work");
918
+ }
919
+ const requiredFields = new Set(aggregation.groupBy ?? []);
920
+ for (const metric of aggregation.metrics) {
921
+ if (metric.field !== "*")
922
+ requiredFields.add(metric.field);
923
+ }
924
+ if (requiredFields.size === 0 && primaryKey)
925
+ requiredFields.add(primaryKey);
926
+ const { aggregation: _aggregation, orderBy: _orderBy, pagination: _pagination, outFields: _outFields, ...base } = query;
927
+ return deepFreeze({
928
+ ...base,
929
+ ...(requiredFields.size > 0 ? { outFields: [...requiredFields].sort() } : {}),
930
+ ...(!options.preserveGeometry ? { returnGeometry: false } : {}),
931
+ // The OGC, OData, and WFS queryAll adapters own the overflow sentinel: a
932
+ // logical N-row ceiling becomes an N + 1 wire request and is reported
933
+ // through exceededTransferLimit. GeoServices retains its established
934
+ // explicit sentinel in the canonical step for backward-compatible output.
935
+ pagination: { offset: 0, limit: maxRows + (options.adapterOwnsLookahead ? 0 : 1) },
936
+ });
937
+ }