@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.
- package/README.md +2 -0
- package/columnar/apache-arrow.d.ts +17 -0
- package/columnar/apache-arrow.d.ts.map +1 -0
- package/columnar/apache-arrow.js +1036 -0
- package/columnar/batch-cache-conformance.d.ts +66 -0
- package/columnar/batch-cache-conformance.d.ts.map +1 -0
- package/columnar/batch-cache-conformance.js +317 -0
- package/columnar/batch-cache-indexeddb.d.ts +31 -0
- package/columnar/batch-cache-indexeddb.d.ts.map +1 -0
- package/columnar/batch-cache-indexeddb.js +148 -0
- package/columnar/batch-cache.d.ts +298 -0
- package/columnar/batch-cache.d.ts.map +1 -0
- package/columnar/batch-cache.js +757 -0
- package/columnar/geoarrow-aggregate.d.ts +178 -0
- package/columnar/geoarrow-aggregate.d.ts.map +1 -0
- package/columnar/geoarrow-aggregate.js +976 -0
- package/columnar/geoarrow-patch.d.ts +382 -0
- package/columnar/geoarrow-patch.d.ts.map +1 -0
- package/columnar/geoarrow-patch.js +1656 -0
- package/columnar/geoarrow-reproject.d.ts +22 -0
- package/columnar/geoarrow-reproject.d.ts.map +1 -0
- package/columnar/geoarrow-reproject.js +119 -0
- package/columnar/geoarrow-serialization.d.ts +68 -0
- package/columnar/geoarrow-serialization.d.ts.map +1 -0
- package/columnar/geoarrow-serialization.js +331 -0
- package/columnar/geoarrow-types.d.ts +271 -0
- package/columnar/geoarrow-types.d.ts.map +1 -0
- package/columnar/geoarrow-types.js +15 -0
- package/columnar/geoarrow.d.ts +52 -0
- package/columnar/geoarrow.d.ts.map +1 -0
- package/columnar/geoarrow.js +1469 -0
- package/columnar/index.d.ts +43 -0
- package/columnar/index.d.ts.map +1 -0
- package/columnar/index.js +29 -0
- package/columnar/operators.d.ts +70 -0
- package/columnar/operators.d.ts.map +1 -0
- package/columnar/operators.js +307 -0
- package/columnar/result.d.ts +369 -0
- package/columnar/result.d.ts.map +1 -0
- package/columnar/result.js +815 -0
- package/columnar/telemetry.d.ts +152 -0
- package/columnar/telemetry.d.ts.map +1 -0
- package/columnar/telemetry.js +265 -0
- package/columnar/transfer.d.ts +28 -0
- package/columnar/transfer.d.ts.map +1 -0
- package/columnar/transfer.js +669 -0
- package/columnar/types.d.ts +175 -0
- package/columnar/types.d.ts.map +1 -0
- package/columnar/types.js +22 -0
- package/columnar/worker.d.ts +157 -0
- package/columnar/worker.d.ts.map +1 -0
- package/columnar/worker.js +1059 -0
- package/connect-url-safety.d.ts +53 -0
- package/connect-url-safety.js +189 -0
- package/contract/pmtiles.d.ts +6 -5
- package/contract/pmtiles.d.ts.map +1 -1
- package/contract/pmtiles.js +30 -5
- package/core/client.d.ts.map +1 -1
- package/core/client.js +87 -19
- package/core/deprecation-notice.d.ts +28 -0
- package/core/deprecation-notice.d.ts.map +1 -0
- package/core/deprecation-notice.js +22 -0
- package/core/esri-geojson.d.ts.map +1 -1
- package/core/esri-geojson.js +10 -6
- package/core/optional-grpc-peer.d.ts +15 -0
- package/core/optional-grpc-peer.d.ts.map +1 -0
- package/core/optional-grpc-peer.js +60 -0
- package/esri-compat/basemap-gallery.d.ts +2 -0
- package/esri-compat/basemap-gallery.d.ts.map +1 -1
- package/esri-compat/basemap-gallery.js +12 -0
- package/esri-compat/basemap.d.ts +4 -0
- package/esri-compat/basemap.d.ts.map +1 -1
- package/esri-compat/basemap.js +70 -0
- package/esri-compat/bookmarks.d.ts +2 -0
- package/esri-compat/bookmarks.d.ts.map +1 -1
- package/esri-compat/bookmarks.js +12 -0
- package/esri-compat/controls.d.ts +18 -0
- package/esri-compat/controls.d.ts.map +1 -1
- package/esri-compat/controls.js +120 -57
- package/esri-compat/directions.d.ts +4 -0
- package/esri-compat/directions.d.ts.map +1 -1
- package/esri-compat/directions.js +30 -20
- package/esri-compat/editor.d.ts +4 -0
- package/esri-compat/editor.d.ts.map +1 -1
- package/esri-compat/editor.js +16 -0
- package/esri-compat/esri-config.d.ts +2 -1
- package/esri-compat/esri-config.d.ts.map +1 -1
- package/esri-compat/feature-effect.d.ts +23 -0
- package/esri-compat/feature-effect.d.ts.map +1 -0
- package/esri-compat/feature-effect.js +12 -0
- package/esri-compat/feature-form.d.ts +6 -0
- package/esri-compat/feature-form.d.ts.map +1 -1
- package/esri-compat/feature-form.js +63 -0
- package/esri-compat/feature-layer.d.ts +21 -2
- package/esri-compat/feature-layer.d.ts.map +1 -1
- package/esri-compat/feature-layer.js +172 -1
- package/esri-compat/feature-table.d.ts +5 -0
- package/esri-compat/feature-table.d.ts.map +1 -1
- package/esri-compat/feature-table.js +23 -0
- package/esri-compat/identity-manager.d.ts +1 -1
- package/esri-compat/identity-manager.js +1 -1
- package/esri-compat/legend.d.ts +2 -0
- package/esri-compat/legend.d.ts.map +1 -1
- package/esri-compat/legend.js +98 -0
- package/esri-compat/locator.d.ts +1 -1
- package/esri-compat/locator.js +1 -1
- package/esri-compat/map-view-mount.d.ts +1 -0
- package/esri-compat/map-view-mount.d.ts.map +1 -1
- package/esri-compat/map-view-mount.js +46 -25
- package/esri-compat/map-view.d.ts +13 -0
- package/esri-compat/map-view.d.ts.map +1 -1
- package/esri-compat/map-view.js +94 -8
- package/esri-compat/map.d.ts +5 -1
- package/esri-compat/map.d.ts.map +1 -1
- package/esri-compat/map.js +10 -0
- package/esri-compat/measurement.d.ts +2 -0
- package/esri-compat/measurement.d.ts.map +1 -1
- package/esri-compat/measurement.js +14 -0
- package/esri-compat/point.d.ts +7 -0
- package/esri-compat/point.d.ts.map +1 -1
- package/esri-compat/point.js +33 -3
- package/esri-compat/popup.d.ts +2 -0
- package/esri-compat/popup.d.ts.map +1 -1
- package/esri-compat/popup.js +12 -0
- package/esri-compat/portal.d.ts +1 -1
- package/esri-compat/portal.js +1 -1
- package/esri-compat/print.d.ts +3 -0
- package/esri-compat/print.d.ts.map +1 -1
- package/esri-compat/print.js +15 -0
- package/esri-compat/search.d.ts +4 -0
- package/esri-compat/search.d.ts.map +1 -1
- package/esri-compat/search.js +25 -28
- package/esri-compat/sketch.d.ts +2 -0
- package/esri-compat/sketch.d.ts.map +1 -1
- package/esri-compat/sketch.js +12 -0
- package/esri-compat/widget-host.d.ts +46 -17
- package/esri-compat/widget-host.d.ts.map +1 -1
- package/esri-compat/widget-host.js +84 -20
- package/esri-compat/widget-shell-hosts.d.ts +61 -0
- package/esri-compat/widget-shell-hosts.d.ts.map +1 -0
- package/esri-compat/widget-shell-hosts.js +76 -0
- package/index.d.ts +3 -1
- package/index.js +1 -0
- package/offline/digest.d.ts +15 -0
- package/offline/digest.js +58 -0
- package/offline/quota.d.ts +161 -0
- package/offline/quota.js +370 -0
- package/offline/types.d.ts +349 -0
- package/offline/types.js +75 -0
- package/package.json +21 -2
- package/query-planner/canonical-filter.d.ts +30 -0
- package/query-planner/canonical-filter.d.ts.map +1 -0
- package/query-planner/canonical-filter.js +30 -0
- package/query-planner/canonical.d.ts +7 -0
- package/query-planner/canonical.d.ts.map +1 -0
- package/query-planner/canonical.js +145 -0
- package/query-planner/columnar-plan.d.ts +63 -0
- package/query-planner/columnar-plan.d.ts.map +1 -0
- package/query-planner/columnar-plan.js +94 -0
- package/query-planner/cql2-json.d.ts +17 -0
- package/query-planner/cql2-json.d.ts.map +1 -0
- package/query-planner/cql2-json.js +646 -0
- package/query-planner/diagnostics.d.ts +35 -0
- package/query-planner/diagnostics.d.ts.map +1 -0
- package/query-planner/diagnostics.js +386 -0
- package/query-planner/duckdb.d.ts +79 -0
- package/query-planner/duckdb.d.ts.map +1 -0
- package/query-planner/duckdb.js +772 -0
- package/query-planner/execution-context.d.ts +5 -0
- package/query-planner/execution-context.d.ts.map +1 -0
- package/query-planner/execution-context.js +52 -0
- package/query-planner/executor.d.ts +5 -0
- package/query-planner/executor.d.ts.map +1 -0
- package/query-planner/executor.js +229 -0
- package/query-planner/geoservices.d.ts +91 -0
- package/query-planner/geoservices.d.ts.map +1 -0
- package/query-planner/geoservices.js +688 -0
- package/query-planner/grpc.d.ts +100 -0
- package/query-planner/grpc.d.ts.map +1 -0
- package/query-planner/grpc.js +760 -0
- package/query-planner/index.d.ts +59 -0
- package/query-planner/index.d.ts.map +1 -0
- package/query-planner/index.js +43 -0
- package/query-planner/ir.d.ts +25 -0
- package/query-planner/ir.d.ts.map +1 -0
- package/query-planner/ir.js +479 -0
- package/query-planner/local-aggregate.d.ts +5 -0
- package/query-planner/local-aggregate.d.ts.map +1 -0
- package/query-planner/local-aggregate.js +126 -0
- package/query-planner/odata-protocol-module.d.ts +54 -0
- package/query-planner/odata-protocol-module.d.ts.map +1 -0
- package/query-planner/odata-protocol-module.js +199 -0
- package/query-planner/odata-v1.d.ts +23 -0
- package/query-planner/odata-v1.d.ts.map +1 -0
- package/query-planner/odata-v1.js +202 -0
- package/query-planner/odata.d.ts +45 -0
- package/query-planner/odata.d.ts.map +1 -0
- package/query-planner/odata.js +519 -0
- package/query-planner/ogc-compiler.d.ts +55 -0
- package/query-planner/ogc-compiler.d.ts.map +1 -0
- package/query-planner/ogc-compiler.js +480 -0
- package/query-planner/ogc-features.d.ts +53 -0
- package/query-planner/ogc-features.d.ts.map +1 -0
- package/query-planner/ogc-features.js +639 -0
- package/query-planner/planner.d.ts +17 -0
- package/query-planner/planner.d.ts.map +1 -0
- package/query-planner/planner.js +937 -0
- package/query-planner/representation.d.ts +59 -0
- package/query-planner/representation.d.ts.map +1 -0
- package/query-planner/representation.js +140 -0
- package/query-planner/resource.d.ts +94 -0
- package/query-planner/resource.d.ts.map +1 -0
- package/query-planner/resource.js +526 -0
- package/query-planner/semantic-canonical.d.ts +15 -0
- package/query-planner/semantic-canonical.d.ts.map +1 -0
- package/query-planner/semantic-canonical.js +62 -0
- package/query-planner/semantic-compat.d.ts +10 -0
- package/query-planner/semantic-compat.d.ts.map +1 -0
- package/query-planner/semantic-compat.js +35 -0
- package/query-planner/semantic-compiler.d.ts +64 -0
- package/query-planner/semantic-compiler.d.ts.map +1 -0
- package/query-planner/semantic-compiler.js +81 -0
- package/query-planner/semantic-literals.d.ts +30 -0
- package/query-planner/semantic-literals.d.ts.map +1 -0
- package/query-planner/semantic-literals.js +133 -0
- package/query-planner/semantic-types.d.ts +265 -0
- package/query-planner/semantic-types.d.ts.map +1 -0
- package/query-planner/semantic-types.js +1 -0
- package/query-planner/semantic.d.ts +54 -0
- package/query-planner/semantic.d.ts.map +1 -0
- package/query-planner/semantic.js +1421 -0
- package/query-planner/types.d.ts +712 -0
- package/query-planner/types.d.ts.map +1 -0
- package/query-planner/types.js +49 -0
- package/query-planner/wfs-protocol-module.d.ts +57 -0
- package/query-planner/wfs-protocol-module.d.ts.map +1 -0
- package/query-planner/wfs-protocol-module.js +731 -0
- package/query-planner/wfs-semantic.d.ts +58 -0
- package/query-planner/wfs-semantic.d.ts.map +1 -0
- package/query-planner/wfs-semantic.js +547 -0
- package/query-planner/wfs-v1.d.ts +41 -0
- package/query-planner/wfs-v1.d.ts.map +1 -0
- package/query-planner/wfs-v1.js +366 -0
- package/query-planner/wfs.d.ts +4 -0
- package/query-planner/wfs.d.ts.map +1 -0
- package/query-planner/wfs.js +2 -0
- package/widget-capabilities.d.ts +67 -0
- package/widget-capabilities.js +152 -0
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
import { compileQueryFilterToCql2Text, compileTemporalFilterToOgcDatetime, } from "../contract/query-filter.js";
|
|
2
|
+
import { geometryToWkt as esriGeometryToWkt } from "../core/odata.js";
|
|
3
|
+
import { canonicalFilterParts } from "./canonical-filter.js";
|
|
4
|
+
import { canonicalStringify, toJsonValue } from "./canonical.js";
|
|
5
|
+
import { bboxInDefinitionOrder, compiledRequestFingerprint, compilerEvidenceFingerprint, compilerField, cql2FieldName, cql2Identifier, cql2StringLiteral, geometryInDefinitionOrder, geometryToWkt, hasConformanceClass, normalizeConformanceUris, normalizeIdentifierUris, ogcAxisPlan, ogcCrsUri, ogcNumber, sharedFilterCrs, } from "./ogc-compiler.js";
|
|
6
|
+
import { hashSemanticQuery } from "./semantic-canonical.js";
|
|
7
|
+
import { prepareSemanticCompilerQuery, runSemanticCompiler, semanticUnsupported, } from "./semantic-compiler.js";
|
|
8
|
+
import { HonuaQueryPlanningError } from "./types.js";
|
|
9
|
+
const OGC_CAPABILITY_FINGERPRINT_DOMAIN = "honua.ogc-features.filter-capabilities.v1";
|
|
10
|
+
const OGC_REQUEST_FINGERPRINT_DOMAIN = "honua.ogc-features.semantic-request.v1";
|
|
11
|
+
const CQL2_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
12
|
+
const CQL2_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/;
|
|
13
|
+
const EXACT_NUMERIC_TEXT = /^-?(?:0|[1-9]\d*)(?:\.\d+)?$/;
|
|
14
|
+
const COMPARISON_TEXT = {
|
|
15
|
+
eq: "=",
|
|
16
|
+
ne: "<>",
|
|
17
|
+
lt: "<",
|
|
18
|
+
lte: "<=",
|
|
19
|
+
gt: ">",
|
|
20
|
+
gte: ">=",
|
|
21
|
+
};
|
|
22
|
+
const SPATIAL_TEXT = {
|
|
23
|
+
equals: "S_EQUALS",
|
|
24
|
+
intersects: "S_INTERSECTS",
|
|
25
|
+
within: "S_WITHIN",
|
|
26
|
+
contains: "S_CONTAINS",
|
|
27
|
+
disjoint: "S_DISJOINT",
|
|
28
|
+
touches: "S_TOUCHES",
|
|
29
|
+
overlaps: "S_OVERLAPS",
|
|
30
|
+
crosses: "S_CROSSES",
|
|
31
|
+
};
|
|
32
|
+
const SPATIAL_JSON = {
|
|
33
|
+
equals: "s_equals",
|
|
34
|
+
intersects: "s_intersects",
|
|
35
|
+
within: "s_within",
|
|
36
|
+
contains: "s_contains",
|
|
37
|
+
disjoint: "s_disjoint",
|
|
38
|
+
touches: "s_touches",
|
|
39
|
+
overlaps: "s_overlaps",
|
|
40
|
+
crosses: "s_crosses",
|
|
41
|
+
};
|
|
42
|
+
const TEMPORAL_TEXT = {
|
|
43
|
+
before: "T_BEFORE",
|
|
44
|
+
after: "T_AFTER",
|
|
45
|
+
during: "T_DURING",
|
|
46
|
+
"time-intersects": "T_INTERSECTS",
|
|
47
|
+
};
|
|
48
|
+
const TEMPORAL_JSON = {
|
|
49
|
+
before: "t_before",
|
|
50
|
+
after: "t_after",
|
|
51
|
+
during: "t_during",
|
|
52
|
+
"time-intersects": "t_intersects",
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Compile canonical query IR to an OGC API Features `/items` request. The
|
|
56
|
+
* compiler is side-effect free and deliberately supports only semantics the
|
|
57
|
+
* first-party OGC adapter can preserve exactly.
|
|
58
|
+
*/
|
|
59
|
+
export function compileOgcApiFeaturesQuery(source, query) {
|
|
60
|
+
if (source.protocol !== "ogc-features") {
|
|
61
|
+
throw new HonuaQueryPlanningError("unsupported-compiler", `ogc-api-features-query-v1 does not compile protocol "${source.protocol}"`);
|
|
62
|
+
}
|
|
63
|
+
if (source.collectionId === undefined || source.collectionId === "") {
|
|
64
|
+
throw new HonuaQueryPlanningError("invalid-query", `Source "${source.id}" requires locator.collectionId for OGC API Features planning`);
|
|
65
|
+
}
|
|
66
|
+
if (query.aggregation) {
|
|
67
|
+
throw new HonuaQueryPlanningError("unsupported-query", "OGC API Features does not provide remote aggregation; use degraded policy with an explicit bounded-local fallback");
|
|
68
|
+
}
|
|
69
|
+
if (query.returnGeometry === false) {
|
|
70
|
+
throw new HonuaQueryPlanningError("unsupported-query", "OGC API Features /items has no portable geometry-suppression parameter; omit returnGeometry or select another protocol");
|
|
71
|
+
}
|
|
72
|
+
const ctx = {
|
|
73
|
+
protocol: source.protocol,
|
|
74
|
+
sourceId: source.id,
|
|
75
|
+
geometryProperty: source.geometryProperty ?? "geometry",
|
|
76
|
+
};
|
|
77
|
+
const parts = canonicalFilterParts(query, ctx);
|
|
78
|
+
const typedFilter = parts.expression
|
|
79
|
+
? compileQueryFilterToCql2Text(parts.expression, ctx, esriGeometryToWkt)
|
|
80
|
+
: undefined;
|
|
81
|
+
const filterParts = [
|
|
82
|
+
...(query.where ? [query.where.expression] : []),
|
|
83
|
+
...(typedFilter !== undefined ? [typedFilter] : []),
|
|
84
|
+
];
|
|
85
|
+
const filter = filterParts.length === 0
|
|
86
|
+
? undefined
|
|
87
|
+
: filterParts.length === 1
|
|
88
|
+
? filterParts[0]
|
|
89
|
+
: filterParts.map((part) => `(${part})`).join(" AND ");
|
|
90
|
+
return {
|
|
91
|
+
compiler: "ogc-api-features-query-v1",
|
|
92
|
+
collectionId: source.collectionId,
|
|
93
|
+
...(filter !== undefined ? { filter, filterLang: "cql2-text" } : {}),
|
|
94
|
+
...(parts.protocolTime ? { datetime: compileTemporalFilterToOgcDatetime(parts.protocolTime, ctx) } : {}),
|
|
95
|
+
...(query.outFields && query.outFields.length > 0 ? { properties: query.outFields } : {}),
|
|
96
|
+
...(query.orderBy && query.orderBy.length > 0
|
|
97
|
+
? { sortby: query.orderBy.map((sort) => `${sort.direction === "desc" ? "-" : ""}${sort.field}`).join(",") }
|
|
98
|
+
: {}),
|
|
99
|
+
...(query.spatialFilter ? { bbox: compileBbox(query.spatialFilter, source.id) } : {}),
|
|
100
|
+
...(query.outSr !== undefined ? { crs: String(query.outSr) } : {}),
|
|
101
|
+
...(query.pagination?.offset !== undefined ? { offset: query.pagination.offset } : {}),
|
|
102
|
+
...(query.pagination?.limit !== undefined ? { limit: query.pagination.limit } : {}),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** Compile a typed semantic query using only explicitly discovered OGC/CQL2 support. */
|
|
106
|
+
export function compileSemanticOgcApiFeaturesQuery(options) {
|
|
107
|
+
return runSemanticCompiler(() => {
|
|
108
|
+
const { query, schema } = prepareSemanticCompilerQuery(options.query, options.schema, "ogc-features");
|
|
109
|
+
const source = verifiedSemanticOgcSource(options.source);
|
|
110
|
+
const conformance = normalizeOgcConformance(options.conformance);
|
|
111
|
+
const language = query.filter
|
|
112
|
+
? selectCql2Language(query.filter, conformance, options.preferredFilterLanguage)
|
|
113
|
+
: undefined;
|
|
114
|
+
const state = language
|
|
115
|
+
? { schema, conformance, language, usesNativeFilter: false }
|
|
116
|
+
: undefined;
|
|
117
|
+
const filter = state && query.filter ? compileCql2Filter(query.filter, state, "$.filter") : undefined;
|
|
118
|
+
const filterCrs = state?.filterCrs
|
|
119
|
+
? compiledOgcFilterCrs(state.filterCrs, conformance, "$.filter")
|
|
120
|
+
: options.nativeFilterCrs && query.filter?.kind === "native"
|
|
121
|
+
? compiledOgcFilterCrs(options.nativeFilterCrs, conformance, "options.nativeFilterCrs")
|
|
122
|
+
: undefined;
|
|
123
|
+
validateOgcSemanticProjection(query, schema);
|
|
124
|
+
rejectOgcSemanticSort(query);
|
|
125
|
+
const page = query.page;
|
|
126
|
+
if (page?.kind === "offset") {
|
|
127
|
+
semanticUnsupported("unsupported-node", "$.page.offset", "OGC API Features Core does not define a client-supplied offset parameter");
|
|
128
|
+
}
|
|
129
|
+
const crs = query.outputCrs ? compiledOgcOutputCrs(query.outputCrs, conformance, "$.outputCrs") : undefined;
|
|
130
|
+
const queryFingerprint = hashSemanticQuery(query, { schema, protocol: "ogc-features" });
|
|
131
|
+
const preimage = {
|
|
132
|
+
compiler: "ogc-api-features-semantic-query-v1",
|
|
133
|
+
...(language ? { dialect: language } : {}),
|
|
134
|
+
schemaFingerprint: schema.fingerprint,
|
|
135
|
+
queryFingerprint,
|
|
136
|
+
capabilityFingerprint: conformance.fingerprint,
|
|
137
|
+
collectionId: source.collectionId,
|
|
138
|
+
...(filter !== undefined ? { filter, filterLang: language } : {}),
|
|
139
|
+
...(filterCrs ? { filterCrs } : {}),
|
|
140
|
+
...(crs ? { crs } : {}),
|
|
141
|
+
...(page?.limit !== undefined ? { limit: page.limit } : {}),
|
|
142
|
+
usesNativeFilter: state?.usesNativeFilter ?? false,
|
|
143
|
+
};
|
|
144
|
+
const artifact = {
|
|
145
|
+
...preimage,
|
|
146
|
+
requestFingerprint: compiledRequestFingerprint(OGC_REQUEST_FINGERPRINT_DOMAIN, toJsonValue(preimage)),
|
|
147
|
+
};
|
|
148
|
+
return { artifact };
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
function verifiedSemanticOgcSource(value) {
|
|
152
|
+
if (!value || typeof value !== "object") {
|
|
153
|
+
throw new HonuaQueryPlanningError("invalid-query", "Semantic OGC source identity is invalid");
|
|
154
|
+
}
|
|
155
|
+
if ((typeof value.collectionId !== "string" && typeof value.collectionId !== "number") ||
|
|
156
|
+
(typeof value.collectionId === "string" &&
|
|
157
|
+
(value.collectionId.length === 0 || value.collectionId.length > 1_024 || hasControl(value.collectionId))) ||
|
|
158
|
+
(typeof value.collectionId === "number" && (!Number.isSafeInteger(value.collectionId) || value.collectionId < 0))) {
|
|
159
|
+
throw new HonuaQueryPlanningError("invalid-query", "options.source.collectionId is invalid");
|
|
160
|
+
}
|
|
161
|
+
return { collectionId: value.collectionId };
|
|
162
|
+
}
|
|
163
|
+
function normalizeOgcConformance(value) {
|
|
164
|
+
if (!value || typeof value !== "object") {
|
|
165
|
+
throw new HonuaQueryPlanningError("invalid-query", "options.conformance is invalid");
|
|
166
|
+
}
|
|
167
|
+
const conformsTo = normalizeConformanceUris(value.conformsTo, "options.conformance.conformsTo");
|
|
168
|
+
const supportedFilterCrs = normalizeIdentifierUris(value.supportedFilterCrs, "options.conformance.supportedFilterCrs");
|
|
169
|
+
const supportedOutputCrs = normalizeIdentifierUris(value.supportedOutputCrs, "options.conformance.supportedOutputCrs");
|
|
170
|
+
return {
|
|
171
|
+
conformsTo,
|
|
172
|
+
supportedFilterCrs,
|
|
173
|
+
supportedOutputCrs,
|
|
174
|
+
fingerprint: compilerEvidenceFingerprint(OGC_CAPABILITY_FINGERPRINT_DOMAIN, toJsonValue({ conformsTo, supportedFilterCrs, supportedOutputCrs })),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function selectCql2Language(filter, conformance, preferred) {
|
|
178
|
+
requireCqlConformance(conformance, "ogcapi-features-3/1.0", "features-filter", "$.filter");
|
|
179
|
+
requireCqlConformance(conformance, "cql2/1.0", "basic-cql2", "$.filter");
|
|
180
|
+
const advertised = ["cql2-json", "cql2-text"].filter((language) => hasConformanceClass(conformance.conformsTo, "cql2/1.0", language));
|
|
181
|
+
if (filter.kind === "native") {
|
|
182
|
+
const language = filter.dialect;
|
|
183
|
+
if (language !== "cql2-json" && language !== "cql2-text") {
|
|
184
|
+
semanticUnsupported("unsupported-native-filter", "$.filter.dialect", `unsupported native dialect ${language}`);
|
|
185
|
+
}
|
|
186
|
+
if (!advertised.includes(language)) {
|
|
187
|
+
semanticUnsupported("unsupported-native-filter", "$.filter.dialect", `native ${language} requires matching discovered conformance`);
|
|
188
|
+
}
|
|
189
|
+
return language;
|
|
190
|
+
}
|
|
191
|
+
if (preferred !== undefined && preferred !== "cql2-json" && preferred !== "cql2-text") {
|
|
192
|
+
throw new HonuaQueryPlanningError("invalid-query", "options.preferredFilterLanguage is invalid");
|
|
193
|
+
}
|
|
194
|
+
if (preferred !== undefined) {
|
|
195
|
+
if (!advertised.includes(preferred)) {
|
|
196
|
+
semanticUnsupported("unsupported-source", "options.preferredFilterLanguage", `${preferred} was not present in discovered conformance`);
|
|
197
|
+
}
|
|
198
|
+
return preferred;
|
|
199
|
+
}
|
|
200
|
+
const selected = advertised[0];
|
|
201
|
+
if (!selected) {
|
|
202
|
+
semanticUnsupported("unsupported-source", "options.conformance.conformsTo", "OGC filtering requires explicitly discovered cql2-json or cql2-text conformance");
|
|
203
|
+
}
|
|
204
|
+
return selected;
|
|
205
|
+
}
|
|
206
|
+
function compileCql2Filter(filter, state, path) {
|
|
207
|
+
if (filter.kind === "native") {
|
|
208
|
+
state.usesNativeFilter = true;
|
|
209
|
+
if (filter.dialect !== state.language) {
|
|
210
|
+
semanticUnsupported("unsupported-native-filter", `${path}.dialect`, `native ${filter.dialect} cannot compile as ${state.language}`);
|
|
211
|
+
}
|
|
212
|
+
return filter.payload.format === "json" ? canonicalStringify(filter.payload.value) : filter.payload.text;
|
|
213
|
+
}
|
|
214
|
+
if (state.language === "cql2-json") {
|
|
215
|
+
return canonicalStringify(compileCql2JsonNode(filter, state, path));
|
|
216
|
+
}
|
|
217
|
+
return compileCql2TextNode(filter, state, path);
|
|
218
|
+
}
|
|
219
|
+
function compileCql2JsonNode(filter, state, path) {
|
|
220
|
+
switch (filter.kind) {
|
|
221
|
+
case "comparison": {
|
|
222
|
+
const field = compilerField(state.schema, filter.left.name, `${path}.left.name`);
|
|
223
|
+
return {
|
|
224
|
+
op: COMPARISON_TEXT[filter.operator],
|
|
225
|
+
args: [
|
|
226
|
+
{ property: cql2FieldName(state.schema, filter.left.name, `${path}.left.name`) },
|
|
227
|
+
cql2JsonLiteral(filter.right.value, field, `${path}.right.value`),
|
|
228
|
+
],
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
case "boolean":
|
|
232
|
+
if (filter.args.length === 1) {
|
|
233
|
+
return compileCql2JsonNode(filter.args[0], state, `${path}.args[0]`);
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
op: filter.operator,
|
|
237
|
+
args: filter.args.map((entry, index) => compileCql2JsonNode(entry, state, `${path}.args[${index}]`)),
|
|
238
|
+
};
|
|
239
|
+
case "not":
|
|
240
|
+
return { op: "not", args: [compileCql2JsonNode(filter.arg, state, `${path}.arg`)] };
|
|
241
|
+
case "null": {
|
|
242
|
+
const node = {
|
|
243
|
+
op: "isNull",
|
|
244
|
+
args: [{ property: cql2FieldName(state.schema, filter.operand.name, `${path}.operand.name`) }],
|
|
245
|
+
};
|
|
246
|
+
return filter.operator === "is-null" ? node : { op: "not", args: [node] };
|
|
247
|
+
}
|
|
248
|
+
case "list": {
|
|
249
|
+
requireCqlConformance(state.conformance, "cql2/1.0", "advanced-comparison-operators", path);
|
|
250
|
+
const field = compilerField(state.schema, filter.operand.name, `${path}.operand.name`);
|
|
251
|
+
return {
|
|
252
|
+
op: "in",
|
|
253
|
+
args: [
|
|
254
|
+
{ property: cql2FieldName(state.schema, filter.operand.name, `${path}.operand.name`) },
|
|
255
|
+
filter.values.map((entry, index) => cql2JsonLiteral(entry.value, field, `${path}.values[${index}].value`)),
|
|
256
|
+
],
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
case "range": {
|
|
260
|
+
requireCqlConformance(state.conformance, "cql2/1.0", "advanced-comparison-operators", path);
|
|
261
|
+
const field = compilerField(state.schema, filter.operand.name, `${path}.operand.name`);
|
|
262
|
+
return {
|
|
263
|
+
op: "between",
|
|
264
|
+
args: [
|
|
265
|
+
{ property: cql2FieldName(state.schema, filter.operand.name, `${path}.operand.name`) },
|
|
266
|
+
cql2JsonLiteral(filter.lower.value, field, `${path}.lower.value`),
|
|
267
|
+
cql2JsonLiteral(filter.upper.value, field, `${path}.upper.value`),
|
|
268
|
+
],
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
case "pattern": {
|
|
272
|
+
requireCqlConformance(state.conformance, "cql2/1.0", "advanced-comparison-operators", path);
|
|
273
|
+
const property = {
|
|
274
|
+
property: cql2FieldName(state.schema, filter.operand.name, `${path}.operand.name`),
|
|
275
|
+
};
|
|
276
|
+
if (filter.caseSensitive === false) {
|
|
277
|
+
requireCqlConformance(state.conformance, "cql2/1.0", "case-insensitive-comparison", path);
|
|
278
|
+
return {
|
|
279
|
+
op: "like",
|
|
280
|
+
args: [
|
|
281
|
+
{ op: "casei", args: [property] },
|
|
282
|
+
{ op: "casei", args: [filter.pattern] },
|
|
283
|
+
],
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
return { op: "like", args: [property, filter.pattern] };
|
|
287
|
+
}
|
|
288
|
+
case "spatial":
|
|
289
|
+
return compileCql2JsonSpatial(filter, state, path);
|
|
290
|
+
case "temporal":
|
|
291
|
+
rejectCql2During(filter.operator, path);
|
|
292
|
+
requireCqlConformance(state.conformance, "cql2/1.0", "temporal-functions", path);
|
|
293
|
+
return {
|
|
294
|
+
op: TEMPORAL_JSON[filter.operator],
|
|
295
|
+
args: [
|
|
296
|
+
{ property: cql2FieldName(state.schema, filter.operand.name, `${path}.operand.name`) },
|
|
297
|
+
cql2JsonTemporal(filter.value, `${path}.value`),
|
|
298
|
+
],
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
function compileCql2TextNode(filter, state, path) {
|
|
303
|
+
switch (filter.kind) {
|
|
304
|
+
case "comparison": {
|
|
305
|
+
const field = compilerField(state.schema, filter.left.name, `${path}.left.name`);
|
|
306
|
+
const property = cql2TextProperty(state.schema, filter.left.name, `${path}.left.name`);
|
|
307
|
+
return `${property} ${COMPARISON_TEXT[filter.operator]} ${cql2TextLiteral(filter.right.value, field, `${path}.right.value`)}`;
|
|
308
|
+
}
|
|
309
|
+
case "boolean":
|
|
310
|
+
if (filter.args.length === 1) {
|
|
311
|
+
return compileCql2TextNode(filter.args[0], state, `${path}.args[0]`);
|
|
312
|
+
}
|
|
313
|
+
return `(${filter.args
|
|
314
|
+
.map((entry, index) => compileCql2TextNode(entry, state, `${path}.args[${index}]`))
|
|
315
|
+
.join(` ${filter.operator.toUpperCase()} `)})`;
|
|
316
|
+
case "not":
|
|
317
|
+
return `NOT (${compileCql2TextNode(filter.arg, state, `${path}.arg`)})`;
|
|
318
|
+
case "null": {
|
|
319
|
+
const property = cql2TextProperty(state.schema, filter.operand.name, `${path}.operand.name`);
|
|
320
|
+
return `${property} IS ${filter.operator === "is-not-null" ? "NOT " : ""}NULL`;
|
|
321
|
+
}
|
|
322
|
+
case "list": {
|
|
323
|
+
requireCqlConformance(state.conformance, "cql2/1.0", "advanced-comparison-operators", path);
|
|
324
|
+
const field = compilerField(state.schema, filter.operand.name, `${path}.operand.name`);
|
|
325
|
+
const values = filter.values
|
|
326
|
+
.map((entry, index) => cql2TextLiteral(entry.value, field, `${path}.values[${index}].value`))
|
|
327
|
+
.join(", ");
|
|
328
|
+
return `${cql2TextProperty(state.schema, filter.operand.name, `${path}.operand.name`)} IN (${values})`;
|
|
329
|
+
}
|
|
330
|
+
case "range": {
|
|
331
|
+
requireCqlConformance(state.conformance, "cql2/1.0", "advanced-comparison-operators", path);
|
|
332
|
+
const field = compilerField(state.schema, filter.operand.name, `${path}.operand.name`);
|
|
333
|
+
return `${cql2TextProperty(state.schema, filter.operand.name, `${path}.operand.name`)} BETWEEN ${cql2TextLiteral(filter.lower.value, field, `${path}.lower.value`)} AND ${cql2TextLiteral(filter.upper.value, field, `${path}.upper.value`)}`;
|
|
334
|
+
}
|
|
335
|
+
case "pattern": {
|
|
336
|
+
requireCqlConformance(state.conformance, "cql2/1.0", "advanced-comparison-operators", path);
|
|
337
|
+
const property = cql2TextProperty(state.schema, filter.operand.name, `${path}.operand.name`);
|
|
338
|
+
const pattern = cql2StringLiteral(filter.pattern, `${path}.pattern`);
|
|
339
|
+
if (filter.caseSensitive === false) {
|
|
340
|
+
requireCqlConformance(state.conformance, "cql2/1.0", "case-insensitive-comparison", path);
|
|
341
|
+
return `CASEI(${property}) LIKE CASEI(${pattern})`;
|
|
342
|
+
}
|
|
343
|
+
return `${property} LIKE ${pattern}`;
|
|
344
|
+
}
|
|
345
|
+
case "spatial":
|
|
346
|
+
return compileCql2TextSpatial(filter, state, path);
|
|
347
|
+
case "temporal":
|
|
348
|
+
rejectCql2During(filter.operator, path);
|
|
349
|
+
requireCqlConformance(state.conformance, "cql2/1.0", "temporal-functions", path);
|
|
350
|
+
return `${TEMPORAL_TEXT[filter.operator]}(${cql2TextProperty(state.schema, filter.operand.name, `${path}.operand.name`)}, ${cql2TextTemporal(filter.value, `${path}.value`)})`;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
function compileCql2JsonSpatial(filter, state, path) {
|
|
354
|
+
const property = {
|
|
355
|
+
property: cql2FieldName(state.schema, spatialProperty(filter, state.schema, path), `${path}.property.name`),
|
|
356
|
+
};
|
|
357
|
+
if (filter.operator === "within-distance" || filter.operator === "beyond-distance") {
|
|
358
|
+
semanticUnsupported("unsupported-distance", `${path}.operator`, "distance predicates are outside CQL2 1.0");
|
|
359
|
+
}
|
|
360
|
+
if (filter.operator === "bbox-intersects") {
|
|
361
|
+
requireCqlConformance(state.conformance, "cql2/1.0", "basic-spatial-functions", path);
|
|
362
|
+
state.filterCrs = sharedFilterCrs(state.filterCrs, filter.bbox.crs, path);
|
|
363
|
+
return { op: "s_intersects", args: [property, { bbox: bboxInDefinitionOrder(filter.bbox, `${path}.bbox`) }] };
|
|
364
|
+
}
|
|
365
|
+
requireCqlSpatialClass(state.conformance, filter.operator, filter.geometry.geometry.type, path);
|
|
366
|
+
state.filterCrs = sharedFilterCrs(state.filterCrs, filter.geometry.crs, path);
|
|
367
|
+
if (filter.geometry.layout !== "xy" && filter.geometry.layout !== "xyz") {
|
|
368
|
+
semanticUnsupported("unsupported-geometry", `${path}.geometry.layout`, "CQL2 JSON cannot preserve measured coordinate layouts");
|
|
369
|
+
}
|
|
370
|
+
const geometry = geometryInDefinitionOrder(filter.geometry, `${path}.geometry`);
|
|
371
|
+
assertCql2JsonGeometry(geometry, `${path}.geometry.geometry`);
|
|
372
|
+
return { op: SPATIAL_JSON[filter.operator], args: [property, toJsonValue(geometry)] };
|
|
373
|
+
}
|
|
374
|
+
function compileCql2TextSpatial(filter, state, path) {
|
|
375
|
+
const property = cql2TextProperty(state.schema, spatialProperty(filter, state.schema, path), `${path}.property.name`);
|
|
376
|
+
if (filter.operator === "within-distance" || filter.operator === "beyond-distance") {
|
|
377
|
+
semanticUnsupported("unsupported-distance", `${path}.operator`, "distance predicates are outside CQL2 1.0");
|
|
378
|
+
}
|
|
379
|
+
if (filter.operator === "bbox-intersects") {
|
|
380
|
+
requireCqlConformance(state.conformance, "cql2/1.0", "basic-spatial-functions", path);
|
|
381
|
+
state.filterCrs = sharedFilterCrs(state.filterCrs, filter.bbox.crs, path);
|
|
382
|
+
const bounds = bboxInDefinitionOrder(filter.bbox, `${path}.bbox`)
|
|
383
|
+
.map((entry, index) => ogcNumber(entry, `${path}.bbox.box.bounds[${index}]`))
|
|
384
|
+
.join(", ");
|
|
385
|
+
return `S_INTERSECTS(${property}, BBOX(${bounds}))`;
|
|
386
|
+
}
|
|
387
|
+
requireCqlSpatialClass(state.conformance, filter.operator, filter.geometry.geometry.type, path);
|
|
388
|
+
state.filterCrs = sharedFilterCrs(state.filterCrs, filter.geometry.crs, path);
|
|
389
|
+
if (filter.geometry.layout !== "xy" && filter.geometry.layout !== "xyz") {
|
|
390
|
+
semanticUnsupported("unsupported-geometry", `${path}.geometry.layout`, "CQL2 text cannot preserve measured coordinate layouts");
|
|
391
|
+
}
|
|
392
|
+
const geometry = geometryInDefinitionOrder(filter.geometry, `${path}.geometry`);
|
|
393
|
+
return `${SPATIAL_TEXT[filter.operator]}(${property}, ${geometryToWkt(geometry, filter.geometry.layout, `${path}.geometry.geometry`)})`;
|
|
394
|
+
}
|
|
395
|
+
function cql2JsonLiteral(value, field, path) {
|
|
396
|
+
requireCql2ScalarField(field, path);
|
|
397
|
+
if (field.type.kind === "date") {
|
|
398
|
+
if (typeof value !== "string" || !CQL2_DATE.test(value)) {
|
|
399
|
+
semanticUnsupported("unsupported-field-type", path, "CQL2 date comparisons require a full-date string");
|
|
400
|
+
}
|
|
401
|
+
return { date: value };
|
|
402
|
+
}
|
|
403
|
+
if (field.type.kind === "timestamp") {
|
|
404
|
+
if (typeof value !== "string" || !CQL2_TIMESTAMP.test(value)) {
|
|
405
|
+
semanticUnsupported("unsupported-field-type", path, "CQL2 timestamp comparisons require a UTC Z instant");
|
|
406
|
+
}
|
|
407
|
+
return { timestamp: value };
|
|
408
|
+
}
|
|
409
|
+
if ((field.type.kind === "integer" || field.type.kind === "decimal") && field.type.jsonEncoding === "string") {
|
|
410
|
+
semanticUnsupported("unsupported-field-type", path, "CQL2 JSON cannot preserve string-encoded numeric precision; use discovered cql2-text");
|
|
411
|
+
}
|
|
412
|
+
if (value === null || Array.isArray(value) || typeof value === "object") {
|
|
413
|
+
semanticUnsupported("unsupported-field-type", path, "CQL2 scalar literals must be string, number, or boolean");
|
|
414
|
+
}
|
|
415
|
+
return value;
|
|
416
|
+
}
|
|
417
|
+
function cql2TextLiteral(value, field, path) {
|
|
418
|
+
requireCql2ScalarField(field, path);
|
|
419
|
+
if (field.type.kind === "date") {
|
|
420
|
+
if (typeof value !== "string" || !CQL2_DATE.test(value)) {
|
|
421
|
+
semanticUnsupported("unsupported-field-type", path, "CQL2 date comparisons require a full-date string");
|
|
422
|
+
}
|
|
423
|
+
return `DATE(${cql2StringLiteral(value, path)})`;
|
|
424
|
+
}
|
|
425
|
+
if (field.type.kind === "timestamp") {
|
|
426
|
+
if (typeof value !== "string" || !CQL2_TIMESTAMP.test(value)) {
|
|
427
|
+
semanticUnsupported("unsupported-field-type", path, "CQL2 timestamp comparisons require a UTC Z instant");
|
|
428
|
+
}
|
|
429
|
+
return `TIMESTAMP(${cql2StringLiteral(value, path)})`;
|
|
430
|
+
}
|
|
431
|
+
if ((field.type.kind === "integer" || field.type.kind === "decimal") && field.type.jsonEncoding === "string") {
|
|
432
|
+
if (typeof value !== "string" || !EXACT_NUMERIC_TEXT.test(value)) {
|
|
433
|
+
semanticUnsupported("unsupported-field-type", path, "CQL2 numeric text is not an exact decimal token");
|
|
434
|
+
}
|
|
435
|
+
return value;
|
|
436
|
+
}
|
|
437
|
+
if (typeof value === "string")
|
|
438
|
+
return cql2StringLiteral(value, path);
|
|
439
|
+
if (typeof value === "number")
|
|
440
|
+
return ogcNumber(value, path);
|
|
441
|
+
if (typeof value === "boolean")
|
|
442
|
+
return value ? "TRUE" : "FALSE";
|
|
443
|
+
semanticUnsupported("unsupported-field-type", path, "CQL2 scalar literals must be string, number, or boolean");
|
|
444
|
+
}
|
|
445
|
+
function requireCql2ScalarField(field, path) {
|
|
446
|
+
switch (field.type.kind) {
|
|
447
|
+
case "boolean":
|
|
448
|
+
case "integer":
|
|
449
|
+
case "float":
|
|
450
|
+
case "decimal":
|
|
451
|
+
case "string":
|
|
452
|
+
case "uuid":
|
|
453
|
+
case "date":
|
|
454
|
+
case "timestamp":
|
|
455
|
+
return;
|
|
456
|
+
default:
|
|
457
|
+
semanticUnsupported("unsupported-field-type", path, `CQL2 has no exact scalar representation for logical field type ${field.type.kind}`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
function cql2JsonTemporal(value, path) {
|
|
461
|
+
if (value.valueType === "date") {
|
|
462
|
+
requireCql2Date(value.value, `${path}.value`);
|
|
463
|
+
return { date: value.value };
|
|
464
|
+
}
|
|
465
|
+
if (value.valueType === "instant") {
|
|
466
|
+
requireCql2Timestamp(value.value, `${path}.value`);
|
|
467
|
+
return { timestamp: value.value };
|
|
468
|
+
}
|
|
469
|
+
const interval = value;
|
|
470
|
+
interval.value.forEach((endpoint, index) => requireCql2IntervalEndpoint(endpoint, `${path}.value[${index}]`));
|
|
471
|
+
return { interval: [...interval.value] };
|
|
472
|
+
}
|
|
473
|
+
function cql2TextTemporal(value, path) {
|
|
474
|
+
if (value.valueType === "date") {
|
|
475
|
+
requireCql2Date(value.value, `${path}.value`);
|
|
476
|
+
return `DATE(${cql2StringLiteral(value.value, `${path}.value`)})`;
|
|
477
|
+
}
|
|
478
|
+
if (value.valueType === "instant") {
|
|
479
|
+
requireCql2Timestamp(value.value, `${path}.value`);
|
|
480
|
+
return `TIMESTAMP(${cql2StringLiteral(value.value, `${path}.value`)})`;
|
|
481
|
+
}
|
|
482
|
+
const interval = value;
|
|
483
|
+
interval.value.forEach((endpoint, index) => requireCql2IntervalEndpoint(endpoint, `${path}.value[${index}]`));
|
|
484
|
+
return `INTERVAL(${cql2StringLiteral(interval.value[0], `${path}.value[0]`)}, ${cql2StringLiteral(interval.value[1], `${path}.value[1]`)})`;
|
|
485
|
+
}
|
|
486
|
+
function rejectCql2During(operator, path) {
|
|
487
|
+
if (operator === "during") {
|
|
488
|
+
semanticUnsupported("unsupported-node", `${path}.operator`, "CQL2 T_DURING requires an interval-valued first operand; semantic temporal properties are instant-valued");
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
function requireCql2Date(value, path) {
|
|
492
|
+
if (!CQL2_DATE.test(value)) {
|
|
493
|
+
semanticUnsupported("unsupported-field-type", path, "CQL2 temporal dates require a full-date literal");
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
function requireCql2Timestamp(value, path) {
|
|
497
|
+
if (!CQL2_TIMESTAMP.test(value)) {
|
|
498
|
+
semanticUnsupported("unsupported-field-type", path, "CQL2 temporal timestamps require a UTC Z instant");
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
function requireCql2IntervalEndpoint(value, path) {
|
|
502
|
+
if (!CQL2_DATE.test(value) && !CQL2_TIMESTAMP.test(value)) {
|
|
503
|
+
semanticUnsupported("unsupported-field-type", path, "CQL2 temporal interval endpoints require a full-date or UTC Z timestamp");
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
function cql2TextProperty(schema, name, path) {
|
|
507
|
+
return cql2Identifier(cql2FieldName(schema, name, path), path);
|
|
508
|
+
}
|
|
509
|
+
function spatialProperty(filter, schema, path) {
|
|
510
|
+
if (filter.property)
|
|
511
|
+
return filter.property.name;
|
|
512
|
+
if (schema.geometry.state === "known" && schema.geometry.primaryField.state === "known") {
|
|
513
|
+
return schema.geometry.primaryField.field;
|
|
514
|
+
}
|
|
515
|
+
semanticUnsupported("unsupported-source", `${path}.property`, "implicit spatial property requires a known primary geometry field");
|
|
516
|
+
}
|
|
517
|
+
function requireCqlSpatialClass(conformance, operator, geometryType, path) {
|
|
518
|
+
if (operator !== "intersects") {
|
|
519
|
+
requireCqlConformance(conformance, "cql2/1.0", "spatial-functions", path);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
requireCqlConformance(conformance, "cql2/1.0", geometryType === "Point" ? "basic-spatial-functions" : "basic-spatial-functions-plus", path);
|
|
523
|
+
}
|
|
524
|
+
function requireCqlConformance(conformance, standard, name, path) {
|
|
525
|
+
if (!hasConformanceClass(conformance.conformsTo, standard, name)) {
|
|
526
|
+
semanticUnsupported("unsupported-node", path, `${name} was not present in explicit discovered conformance`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
function compiledOgcFilterCrs(binding, conformance, path) {
|
|
530
|
+
const dimensions = binding.coordinateOrder.axes.length >= 3 ? 3 : 2;
|
|
531
|
+
const plan = ogcAxisPlan(binding, dimensions, `${path}.crs`);
|
|
532
|
+
if (isDefaultFilterCrs(plan.srsName, dimensions))
|
|
533
|
+
return undefined;
|
|
534
|
+
requireOgcPart2Crs(conformance, `${path}.crs`);
|
|
535
|
+
if (!conformance.supportedFilterCrs.includes(plan.srsName)) {
|
|
536
|
+
semanticUnsupported("unsupported-crs", `${path}.crs`, `filter CRS ${plan.srsName} was not present in discovered source metadata`);
|
|
537
|
+
}
|
|
538
|
+
return plan.srsName;
|
|
539
|
+
}
|
|
540
|
+
function compiledOgcOutputCrs(definition, conformance, path) {
|
|
541
|
+
const uri = ogcCrsUri(definition, path);
|
|
542
|
+
if (isDefaultOutputCrs(uri))
|
|
543
|
+
return undefined;
|
|
544
|
+
requireOgcPart2Crs(conformance, path);
|
|
545
|
+
if (!conformance.supportedOutputCrs.includes(uri)) {
|
|
546
|
+
semanticUnsupported("unsupported-crs", path, `output CRS ${uri} was not present in discovered source metadata`);
|
|
547
|
+
}
|
|
548
|
+
return uri;
|
|
549
|
+
}
|
|
550
|
+
function requireOgcPart2Crs(conformance, path) {
|
|
551
|
+
if (!hasConformanceClass(conformance.conformsTo, "ogcapi-features-2/1.0", "crs")) {
|
|
552
|
+
semanticUnsupported("unsupported-crs", path, "OGC API Features CRS parameters require explicitly discovered Part 2 CRS conformance");
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
function validateOgcSemanticProjection(query, schema) {
|
|
556
|
+
if (query.kind === "aggregate") {
|
|
557
|
+
semanticUnsupported("unsupported-node", "$.kind", "OGC API Features has no portable remote aggregation");
|
|
558
|
+
}
|
|
559
|
+
if (query.geometry === "omit") {
|
|
560
|
+
semanticUnsupported("unsupported-projection", "$.geometry", "OGC API Features has no portable geometry-suppression request parameter");
|
|
561
|
+
}
|
|
562
|
+
if (query.geometry && typeof query.geometry === "object") {
|
|
563
|
+
const primary = schema.geometry.state === "known" && schema.geometry.primaryField.state === "known"
|
|
564
|
+
? schema.geometry.primaryField.field
|
|
565
|
+
: undefined;
|
|
566
|
+
if (query.geometry.field !== primary) {
|
|
567
|
+
semanticUnsupported("unsupported-projection", "$.geometry.field", "OGC API Features can return only the source primary GeoJSON geometry");
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (query.select && query.select.length > 0) {
|
|
571
|
+
semanticUnsupported("unsupported-projection", "$.select", "OGC API Features Core does not define a portable properties projection parameter");
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
function rejectOgcSemanticSort(query) {
|
|
575
|
+
if (query.sort && query.sort.length > 0) {
|
|
576
|
+
semanticUnsupported("unsupported-sort", "$.sort", "OGC API Features Core does not define a portable sortby parameter");
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
function assertCql2JsonGeometry(value, path) {
|
|
580
|
+
if (value.type !== "GeometryCollection")
|
|
581
|
+
return;
|
|
582
|
+
if (value.geometries.length < 2) {
|
|
583
|
+
semanticUnsupported("unsupported-geometry", `${path}.geometries`, "CQL2 JSON GeometryCollection requires at least two members");
|
|
584
|
+
}
|
|
585
|
+
value.geometries.forEach((entry, index) => {
|
|
586
|
+
if (entry.type === "GeometryCollection") {
|
|
587
|
+
semanticUnsupported("unsupported-geometry", `${path}.geometries[${index}]`, "CQL2 JSON 1.0 does not permit nested GeometryCollection members");
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
function isDefaultFilterCrs(uri, dimensions) {
|
|
592
|
+
return dimensions === 3
|
|
593
|
+
? uri === "http://www.opengis.net/def/crs/OGC/0/CRS84h"
|
|
594
|
+
: uri === "http://www.opengis.net/def/crs/OGC/1.3/CRS84";
|
|
595
|
+
}
|
|
596
|
+
function isDefaultOutputCrs(uri) {
|
|
597
|
+
return (uri === "http://www.opengis.net/def/crs/OGC/1.3/CRS84" || uri === "http://www.opengis.net/def/crs/OGC/0/CRS84h");
|
|
598
|
+
}
|
|
599
|
+
function hasControl(value) {
|
|
600
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: source identifiers cannot contain controls
|
|
601
|
+
return /[\u0000-\u001f\u007f]/.test(value);
|
|
602
|
+
}
|
|
603
|
+
function compileBbox(spatialFilter, sourceId) {
|
|
604
|
+
if (spatialFilter.geometryType !== "esriGeometryEnvelope") {
|
|
605
|
+
throw new HonuaQueryPlanningError("unsupported-query", `Source "${sourceId}" cannot compile spatial geometry "${spatialFilter.geometryType}" to OGC bbox`);
|
|
606
|
+
}
|
|
607
|
+
if (spatialFilter.spatialRel !== undefined &&
|
|
608
|
+
spatialFilter.spatialRel !== "esriSpatialRelIntersects" &&
|
|
609
|
+
spatialFilter.spatialRel !== "esriSpatialRelEnvelopeIntersects") {
|
|
610
|
+
throw new HonuaQueryPlanningError("unsupported-query", `Source "${sourceId}" cannot weaken spatial relationship "${spatialFilter.spatialRel}" to OGC envelope-intersects`);
|
|
611
|
+
}
|
|
612
|
+
const geometry = spatialFilter.geometry;
|
|
613
|
+
assertDefaultBboxCrs(geometry.spatialReference, sourceId);
|
|
614
|
+
const coordinates = [geometry.xmin, geometry.ymin, geometry.xmax, geometry.ymax];
|
|
615
|
+
if (!coordinates.every((value) => typeof value === "number" && Number.isFinite(value))) {
|
|
616
|
+
throw new HonuaQueryPlanningError("invalid-query", `Source "${sourceId}" requires finite xmin, ymin, xmax, and ymax values for OGC bbox`);
|
|
617
|
+
}
|
|
618
|
+
const [xmin, ymin, xmax, ymax] = coordinates;
|
|
619
|
+
if (xmin > xmax || ymin > ymax) {
|
|
620
|
+
throw new HonuaQueryPlanningError("invalid-query", `Source "${sourceId}" requires an ordered OGC bbox where xmin <= xmax and ymin <= ymax`);
|
|
621
|
+
}
|
|
622
|
+
return `${xmin},${ymin},${xmax},${ymax}`;
|
|
623
|
+
}
|
|
624
|
+
function assertDefaultBboxCrs(spatialReference, sourceId) {
|
|
625
|
+
if (spatialReference === undefined)
|
|
626
|
+
return;
|
|
627
|
+
if (spatialReference === null || Array.isArray(spatialReference) || typeof spatialReference !== "object") {
|
|
628
|
+
throw unsupportedBboxCrs(sourceId);
|
|
629
|
+
}
|
|
630
|
+
const reference = spatialReference;
|
|
631
|
+
const declaredWkids = [reference.wkid, reference.latestWkid].filter((value) => value !== undefined);
|
|
632
|
+
if (declaredWkids.length > 0 && declaredWkids.every((value) => value === 4326) && reference.wkt === undefined) {
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
throw unsupportedBboxCrs(sourceId);
|
|
636
|
+
}
|
|
637
|
+
function unsupportedBboxCrs(sourceId) {
|
|
638
|
+
return new HonuaQueryPlanningError("unsupported-query", `Source "${sourceId}" can only compile an unstamped or EPSG:4326 envelope to the default OGC bbox CRS`);
|
|
639
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ExplainGeoParquetQueryOptions, type ExplainQueryOptions, type QueryExecutionPlan, type QueryExecutionPlanV1, type QueryExecutionPlanV2 } from "./types.js";
|
|
2
|
+
export declare function explainQuery<T>(options: ExplainGeoParquetQueryOptions<T>): QueryExecutionPlanV2;
|
|
3
|
+
export declare function explainQuery<T>(options: ExplainQueryOptions<T>): QueryExecutionPlanV1;
|
|
4
|
+
export declare function hashQueryPlan(plan: QueryExecutionPlan): `sha256:${string}`;
|
|
5
|
+
/** @internal Lightweight v1 integrity path for stable consumers that cannot execute v2 resources. */
|
|
6
|
+
export declare function hashQueryPlanV1(plan: QueryExecutionPlanV1): `sha256:${string}` | undefined;
|
|
7
|
+
/** Internal executor boundary: validate once and continue with an immutable detached snapshot. */
|
|
8
|
+
export declare function validateQueryPlanSnapshot(plan: QueryExecutionPlan): QueryExecutionPlan;
|
|
9
|
+
/** Canonical persistence boundary; validated legacy protocol artifacts are upgraded before serialization. */
|
|
10
|
+
export declare function serializeQueryPlan(plan: QueryExecutionPlan): string;
|
|
11
|
+
/** Parse a persisted plan, upgrading validated legacy protocol artifacts without retaining unsafe input. */
|
|
12
|
+
export declare function parseQueryPlan(serialized: string): QueryExecutionPlan;
|
|
13
|
+
/** Stable cache identity derived only after the plan passes the persistence guard. */
|
|
14
|
+
export declare function queryPlanCacheKey(plan: QueryExecutionPlan): string;
|
|
15
|
+
/** Explicitly migrate a credential-free v1 GeoParquet plan to opaque v2 identity. */
|
|
16
|
+
export declare function migrateGeoParquetQueryPlanV1(legacyPlan: QueryExecutionPlanV1, resourceValue: unknown): QueryExecutionPlanV2;
|
|
17
|
+
//# sourceMappingURL=planner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"planner.d.ts","sourceRoot":"","sources":["../../../src/query-planner/planner.ts"],"names":[],"mappings":"AAyBA,OAAO,EAIL,KAAK,6BAA6B,EAClC,KAAK,mBAAmB,EASxB,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EAO1B,MAAM,YAAY,CAAC;AAGpB,wBAAgB,YAAY,CAAC,CAAC,EAAE,OAAO,EAAE,6BAA6B,CAAC,CAAC,CAAC,GAAG,oBAAoB,CAAC;AACjG,wBAAgB,YAAY,CAAC,CAAC,EAAE,OAAO,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAAG,oBAAoB,CAAC;AAwNvF,wBAAgB,aAAa,CAAC,IAAI,EAAE,kBAAkB,GAAG,UAAU,MAAM,EAAE,CAE1E;AAED,qGAAqG;AACrG,wBAAgB,eAAe,CAAC,IAAI,EAAE,oBAAoB,GAAG,UAAU,MAAM,EAAE,GAAG,SAAS,CAkB1F;AAED,kGAAkG;AAClG,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,kBAAkB,GAAG,kBAAkB,CAItF;AAED,6GAA6G;AAC7G,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,kBAAkB,GAAG,MAAM,CAInE;AAED,4GAA4G;AAC5G,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,kBAAkB,CAWrE;AAED,sFAAsF;AACtF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,kBAAkB,GAAG,MAAM,CAIlE;AAED,qFAAqF;AACrF,wBAAgB,4BAA4B,CAC1C,UAAU,EAAE,oBAAoB,EAChC,aAAa,EAAE,OAAO,GACrB,oBAAoB,CAkCtB"}
|