@honua/sdk-esri-compat 0.1.11-beta.0 → 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,760 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile canonical query IR to a deterministic, inspectable description of the
|
|
3
|
+
* `geospatial.v1.FeatureService/QueryFeatures` unary gRPC request — without pulling
|
|
4
|
+
* the `@bufbuild/protobuf` runtime into the planner graph.
|
|
5
|
+
*
|
|
6
|
+
* The compiled object mirrors the generated `QueryFeaturesRequest` message
|
|
7
|
+
* field-for-field (including proto enum *value names* for spatial relationship
|
|
8
|
+
* and statistic type), so it is a faithful, hashable pre-image of the wire
|
|
9
|
+
* request. The live gRPC adapter (`core/grpc-adapter.ts`) turns the same
|
|
10
|
+
* canonical inputs into the protobuf message; this keeps the plan honest about
|
|
11
|
+
* exactly what will be sent.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
import { compileQueryFilterToSql92 } from "../contract/query-filter.js";
|
|
16
|
+
import { validateExecutableCrsBinding } from "../contract/schema.js";
|
|
17
|
+
import { canonicalFilterParts, refuseCanonicalFilter } from "./canonical-filter.js";
|
|
18
|
+
import { hashSemanticQuery } from "./semantic-canonical.js";
|
|
19
|
+
import { prepareSemanticCompilerQuery, runSemanticCompiler, sameExecutableCrs, semanticSchemaField, semanticUnsupported, } from "./semantic-compiler.js";
|
|
20
|
+
import { HonuaQueryPlanningError } from "./types.js";
|
|
21
|
+
const SPATIAL_REL_MAP = {
|
|
22
|
+
esriSpatialRelIntersects: "SPATIAL_RELATIONSHIP_INTERSECTS",
|
|
23
|
+
esriSpatialRelContains: "SPATIAL_RELATIONSHIP_CONTAINS",
|
|
24
|
+
esriSpatialRelWithin: "SPATIAL_RELATIONSHIP_WITHIN",
|
|
25
|
+
esriSpatialRelEnvelopeIntersects: "SPATIAL_RELATIONSHIP_ENVELOPE_INTERSECTS",
|
|
26
|
+
esriSpatialRelIndexIntersects: "SPATIAL_RELATIONSHIP_ENVELOPE_INTERSECTS",
|
|
27
|
+
esriSpatialRelCrosses: "SPATIAL_RELATIONSHIP_CROSSES",
|
|
28
|
+
esriSpatialRelTouches: "SPATIAL_RELATIONSHIP_TOUCHES",
|
|
29
|
+
esriSpatialRelOverlaps: "SPATIAL_RELATIONSHIP_OVERLAPS",
|
|
30
|
+
esriSpatialRelDisjoint: "SPATIAL_RELATIONSHIP_DISJOINT",
|
|
31
|
+
};
|
|
32
|
+
const STATISTIC_TYPE_MAP = {
|
|
33
|
+
count: "STATISTIC_TYPE_COUNT",
|
|
34
|
+
sum: "STATISTIC_TYPE_SUM",
|
|
35
|
+
avg: "STATISTIC_TYPE_AVG",
|
|
36
|
+
min: "STATISTIC_TYPE_MIN",
|
|
37
|
+
max: "STATISTIC_TYPE_MAX",
|
|
38
|
+
stddev: "STATISTIC_TYPE_STDDEV",
|
|
39
|
+
var: "STATISTIC_TYPE_VAR",
|
|
40
|
+
};
|
|
41
|
+
/** Compile canonical query IR to the Honua gRPC `QueryFeatures` request shape. */
|
|
42
|
+
export function compileGrpcQuery(source, query) {
|
|
43
|
+
if (source.protocol !== "grpc") {
|
|
44
|
+
throw new HonuaQueryPlanningError("unsupported-compiler", `honua-grpc-query-features-v1 does not compile protocol "${source.protocol}"`);
|
|
45
|
+
}
|
|
46
|
+
if (source.serviceId === undefined || source.layerId === undefined) {
|
|
47
|
+
throw new HonuaQueryPlanningError("invalid-query", `Source "${source.id}" requires locator.serviceId and locator.layerId for gRPC planning`);
|
|
48
|
+
}
|
|
49
|
+
if (query.aggregation?.histogram || query.aggregation?.timeSeries) {
|
|
50
|
+
throw new HonuaQueryPlanningError("unsupported-query", "The gRPC compiler supports metrics and groupBy; histogram/timeSeries remain follow-on work");
|
|
51
|
+
}
|
|
52
|
+
const aggregation = query.aggregation ? compileAggregation(query.aggregation) : undefined;
|
|
53
|
+
// Aggregation returns statistic rows, never geometry — mirror the FeatureService
|
|
54
|
+
// and GeoServices contract by forcing returnGeometry=false for statistics.
|
|
55
|
+
const returnGeometry = aggregation ? false : query.returnGeometry;
|
|
56
|
+
// The gRPC FeatureService speaks the same SQL-92 `where` vocabulary as the
|
|
57
|
+
// REST FeatureServer, so the typed filter lowers through the same compiler.
|
|
58
|
+
// It has no `time` parameter, so a source-dimension temporal filter refuses.
|
|
59
|
+
const ctx = { protocol: "grpc", sourceId: source.id };
|
|
60
|
+
const parts = canonicalFilterParts(query, ctx);
|
|
61
|
+
if (parts.protocolTime) {
|
|
62
|
+
refuseCanonicalFilter("Query.temporalFilter without a field", "honua gRPC FeatureService", "QueryFeatures has no time parameter; name the temporal field so the predicate stays exact");
|
|
63
|
+
}
|
|
64
|
+
const typed = parts.expression ? compileQueryFilterToSql92(parts.expression, ctx) : {};
|
|
65
|
+
if (query.spatialFilter && typed.spatialFilter) {
|
|
66
|
+
throw new HonuaQueryPlanningError("unsupported-query", "A gRPC QueryFeatures request carries one geometry; Query.spatialFilter and a spatial filter node cannot both be sent");
|
|
67
|
+
}
|
|
68
|
+
const spatialFilter = query.spatialFilter ?? typed.spatialFilter;
|
|
69
|
+
const whereParts = [
|
|
70
|
+
...(query.where ? [query.where.expression] : []),
|
|
71
|
+
...(typed.where !== undefined ? [typed.where] : []),
|
|
72
|
+
];
|
|
73
|
+
const where = whereParts.length === 0
|
|
74
|
+
? undefined
|
|
75
|
+
: whereParts.length === 1
|
|
76
|
+
? whereParts[0]
|
|
77
|
+
: whereParts.map((part) => `(${part})`).join(" AND ");
|
|
78
|
+
return {
|
|
79
|
+
compiler: "honua-grpc-query-features-v1",
|
|
80
|
+
service: "geospatial.v1.FeatureService",
|
|
81
|
+
method: "QueryFeatures",
|
|
82
|
+
serviceId: source.serviceId,
|
|
83
|
+
layerId: source.layerId,
|
|
84
|
+
...(where !== undefined ? { where } : {}),
|
|
85
|
+
...(query.outFields && query.outFields.length > 0 ? { outFields: query.outFields } : {}),
|
|
86
|
+
...(returnGeometry !== undefined ? { returnGeometry } : {}),
|
|
87
|
+
...(query.outSr !== undefined ? { outSr: query.outSr } : {}),
|
|
88
|
+
...(query.orderBy && query.orderBy.length > 0
|
|
89
|
+
? {
|
|
90
|
+
orderBy: query.orderBy.map((sort) => `${sort.field}${sort.direction === "desc" ? " DESC" : ""}`).join(","),
|
|
91
|
+
}
|
|
92
|
+
: {}),
|
|
93
|
+
...(spatialFilter
|
|
94
|
+
? {
|
|
95
|
+
spatialFilter: {
|
|
96
|
+
geometry: spatialFilter.geometry,
|
|
97
|
+
geometryType: spatialFilter.geometryType,
|
|
98
|
+
spatialRelationship: spatialRelationship(spatialFilter.spatialRel),
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
: {}),
|
|
102
|
+
...(query.pagination?.offset !== undefined ? { resultOffsetLong: String(query.pagination.offset) } : {}),
|
|
103
|
+
...(query.pagination?.limit !== undefined ? { resultRecordCountLong: String(query.pagination.limit) } : {}),
|
|
104
|
+
...aggregation,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function spatialRelationship(rel) {
|
|
108
|
+
if (rel === undefined)
|
|
109
|
+
return "SPATIAL_RELATIONSHIP_INTERSECTS";
|
|
110
|
+
const mapped = SPATIAL_REL_MAP[rel];
|
|
111
|
+
if (!mapped) {
|
|
112
|
+
throw new HonuaQueryPlanningError("unsupported-query", `gRPC transport does not support spatial relationship "${rel}"`);
|
|
113
|
+
}
|
|
114
|
+
return mapped;
|
|
115
|
+
}
|
|
116
|
+
function compileAggregation(aggregation) {
|
|
117
|
+
return {
|
|
118
|
+
outStatistics: aggregation.metrics.map((metric) => ({
|
|
119
|
+
statisticType: STATISTIC_TYPE_MAP[metric.fn],
|
|
120
|
+
onStatisticField: metric.field,
|
|
121
|
+
outStatisticFieldName: metric.alias ?? `${metric.fn}_${metric.field}`,
|
|
122
|
+
})),
|
|
123
|
+
...(aggregation.groupBy && aggregation.groupBy.length > 0 ? { groupBy: aggregation.groupBy } : {}),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/** Compile a typed semantic query to the canonical QueryFeatures request shape. */
|
|
127
|
+
export function compileSemanticGrpcQuery(options) {
|
|
128
|
+
return runSemanticCompiler(() => {
|
|
129
|
+
const { query, schema } = prepareSemanticCompilerQuery(options.query, options.schema, "grpc");
|
|
130
|
+
const source = verifiedGrpcSource(options.source);
|
|
131
|
+
const state = { schema, usesNativeFilter: false };
|
|
132
|
+
const parts = query.filter
|
|
133
|
+
? compileGrpcFilterParts(query.filter, state, "$.filter")
|
|
134
|
+
: {};
|
|
135
|
+
const projection = grpcProjection(query, state);
|
|
136
|
+
const spatialFilter = parts.spatial
|
|
137
|
+
? compileGrpcSpatial(parts.spatial.filter, state, parts.spatial.path)
|
|
138
|
+
: undefined;
|
|
139
|
+
const orderBy = grpcOrderBy(query, state);
|
|
140
|
+
const aggregation = query.kind === "aggregate" ? grpcSemanticAggregation(query, state) : {};
|
|
141
|
+
const page = query.page;
|
|
142
|
+
verifyGrpcPage(page);
|
|
143
|
+
const where = parts.where;
|
|
144
|
+
if (where && where.length > 4_000) {
|
|
145
|
+
semanticUnsupported("unsupported-node", "$.filter", "Honua gRPC where clauses are limited to 4000 characters by the server");
|
|
146
|
+
}
|
|
147
|
+
const outSr = query.outputCrs ? grpcSpatialReference(query.outputCrs, "$.outputCrs") : undefined;
|
|
148
|
+
if (outSr && query.kind === "features" && !projection.returnGeometry) {
|
|
149
|
+
semanticUnsupported("unsupported-projection", "$.outputCrs", "Honua gRPC cannot apply an output CRS when geometry is omitted");
|
|
150
|
+
}
|
|
151
|
+
if (outSr && query.kind === "aggregate") {
|
|
152
|
+
semanticUnsupported("unsupported-projection", "$.outputCrs", "Honua gRPC aggregate rows do not carry output geometry");
|
|
153
|
+
}
|
|
154
|
+
const artifact = {
|
|
155
|
+
compiler: "honua-grpc-semantic-query-v1",
|
|
156
|
+
dialect: "honua-grpc",
|
|
157
|
+
schemaFingerprint: schema.fingerprint,
|
|
158
|
+
queryFingerprint: hashSemanticQuery(query, { schema, protocol: "grpc" }),
|
|
159
|
+
service: "geospatial.v1.FeatureService",
|
|
160
|
+
method: "QueryFeatures",
|
|
161
|
+
serviceId: source.serviceId,
|
|
162
|
+
layerId: source.layerId,
|
|
163
|
+
...(where ? { where } : {}),
|
|
164
|
+
...(projection.outFields ? { outFields: projection.outFields } : {}),
|
|
165
|
+
returnGeometry: query.kind === "aggregate" ? false : projection.returnGeometry,
|
|
166
|
+
...(outSr ? { outSr } : {}),
|
|
167
|
+
...(orderBy ? { orderBy } : {}),
|
|
168
|
+
...(page?.kind === "offset" ? { resultOffsetLong: String(page.offset) } : {}),
|
|
169
|
+
...(page?.limit !== undefined ? { resultRecordCountLong: String(page.limit) } : {}),
|
|
170
|
+
...(spatialFilter ? { spatialFilter } : {}),
|
|
171
|
+
...aggregation,
|
|
172
|
+
usesNativeFilter: state.usesNativeFilter,
|
|
173
|
+
};
|
|
174
|
+
return { artifact };
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
function verifiedGrpcSource(value) {
|
|
178
|
+
if (!value || typeof value !== "object") {
|
|
179
|
+
throw new HonuaQueryPlanningError("invalid-query", "Semantic gRPC source identity is invalid");
|
|
180
|
+
}
|
|
181
|
+
if (typeof value.serviceId !== "string" ||
|
|
182
|
+
value.serviceId.trim().length === 0 ||
|
|
183
|
+
value.serviceId.length > 256 ||
|
|
184
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: source identifiers cannot contain control bytes
|
|
185
|
+
/[\u0000-\u001f\u007f]/.test(value.serviceId)) {
|
|
186
|
+
throw new HonuaQueryPlanningError("invalid-query", "options.source.serviceId is invalid");
|
|
187
|
+
}
|
|
188
|
+
if (!Number.isSafeInteger(value.layerId) || value.layerId < 0 || value.layerId > 2_147_483_647) {
|
|
189
|
+
throw new HonuaQueryPlanningError("invalid-query", "options.source.layerId is invalid");
|
|
190
|
+
}
|
|
191
|
+
return { serviceId: value.serviceId, layerId: value.layerId };
|
|
192
|
+
}
|
|
193
|
+
function verifyGrpcPage(page) {
|
|
194
|
+
if (!page)
|
|
195
|
+
return;
|
|
196
|
+
if (page.limit !== undefined && (!Number.isSafeInteger(page.limit) || page.limit < 0)) {
|
|
197
|
+
semanticUnsupported("unsupported-node", "$.page.limit", "QueryFeatures result_record_count must be an exact int64");
|
|
198
|
+
}
|
|
199
|
+
if (page.kind === "offset" && (!Number.isSafeInteger(page.offset) || page.offset < 0)) {
|
|
200
|
+
semanticUnsupported("unsupported-node", "$.page.offset", "QueryFeatures result_offset must be an exact int64");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function grpcProjection(query, state) {
|
|
204
|
+
if (query.kind === "aggregate")
|
|
205
|
+
return { returnGeometry: false };
|
|
206
|
+
const primary = primaryGeometryField(state.schema);
|
|
207
|
+
let geometryField;
|
|
208
|
+
if (query.geometry && typeof query.geometry === "object") {
|
|
209
|
+
geometryField = query.geometry.field;
|
|
210
|
+
}
|
|
211
|
+
else if (query.geometry !== "omit") {
|
|
212
|
+
geometryField = primary;
|
|
213
|
+
}
|
|
214
|
+
if (query.geometry === "include" && !primary) {
|
|
215
|
+
semanticUnsupported("unsupported-projection", "$.geometry", "Honua gRPC geometry inclusion requires a known primary geometry field");
|
|
216
|
+
}
|
|
217
|
+
if (geometryField && geometryField !== primary) {
|
|
218
|
+
semanticUnsupported("unsupported-projection", "$.geometry", "QueryFeatures can return only the source primary geometry field");
|
|
219
|
+
}
|
|
220
|
+
if (!query.select)
|
|
221
|
+
return { returnGeometry: geometryField !== undefined };
|
|
222
|
+
const outFields = [];
|
|
223
|
+
query.select.forEach((name, index) => {
|
|
224
|
+
const field = semanticSchemaField(state.schema, name, `$.select[${index}]`);
|
|
225
|
+
if (field.type.kind === "geometry") {
|
|
226
|
+
if (field.name !== geometryField) {
|
|
227
|
+
semanticUnsupported("unsupported-projection", `$.select[${index}]`, "QueryFeatures cannot return a geometry field as an ordinary attribute");
|
|
228
|
+
}
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
outFields.push(grpcPublicFieldName(field, state, `$.select[${index}]`));
|
|
232
|
+
});
|
|
233
|
+
if (outFields.length === 0) {
|
|
234
|
+
semanticUnsupported("unsupported-projection", "$.select", "QueryFeatures cannot represent an explicit geometry-only projection");
|
|
235
|
+
}
|
|
236
|
+
return { outFields, returnGeometry: geometryField !== undefined };
|
|
237
|
+
}
|
|
238
|
+
function grpcOrderBy(query, state) {
|
|
239
|
+
if (!query.sort || query.sort.length === 0)
|
|
240
|
+
return undefined;
|
|
241
|
+
if (query.kind === "aggregate") {
|
|
242
|
+
const groups = new Set(query.groupBy);
|
|
243
|
+
const unsupportedIndex = query.sort.findIndex((sort) => !groups.has(sort.field));
|
|
244
|
+
if (unsupportedIndex >= 0) {
|
|
245
|
+
semanticUnsupported("unsupported-sort", `$.sort[${unsupportedIndex}].field`, "Honua gRPC aggregate queries may sort only by a grouped source field");
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return query.sort
|
|
249
|
+
.map((sort, index) => {
|
|
250
|
+
if (sort.nulls && sort.nulls !== "native") {
|
|
251
|
+
semanticUnsupported("unsupported-sort", `$.sort[${index}].nulls`, "QueryFeatures does not carry an explicit null ordering");
|
|
252
|
+
}
|
|
253
|
+
const field = semanticSchemaField(state.schema, sort.field, `$.sort[${index}].field`);
|
|
254
|
+
return `${grpcPublicFieldName(field, state, `$.sort[${index}].field`)}${sort.direction === "desc" ? " DESC" : " ASC"}`;
|
|
255
|
+
})
|
|
256
|
+
.join(", ");
|
|
257
|
+
}
|
|
258
|
+
function grpcSemanticAggregation(query, state) {
|
|
259
|
+
const outputNames = new Set();
|
|
260
|
+
const groupBy = query.groupBy.map((name, index) => {
|
|
261
|
+
const publicName = grpcPublicFieldName(semanticSchemaField(state.schema, name, `$.groupBy[${index}]`), state, `$.groupBy[${index}]`);
|
|
262
|
+
const folded = publicName.toLowerCase();
|
|
263
|
+
if (outputNames.has(folded)) {
|
|
264
|
+
semanticUnsupported("unsupported-source", `$.groupBy[${index}]`, "QueryFeatures result fields must be unique under server case folding");
|
|
265
|
+
}
|
|
266
|
+
outputNames.add(folded);
|
|
267
|
+
return publicName;
|
|
268
|
+
});
|
|
269
|
+
const outStatistics = query.metrics.map((metric, index) => {
|
|
270
|
+
const field = metric.field
|
|
271
|
+
? semanticSchemaField(state.schema, metric.field, `$.metrics[${index}].field`)
|
|
272
|
+
: undefined;
|
|
273
|
+
if (!field) {
|
|
274
|
+
semanticUnsupported("unsupported-node", `$.metrics[${index}].field`, "Honua gRPC statistics require a named public source field");
|
|
275
|
+
}
|
|
276
|
+
const statisticType = {
|
|
277
|
+
count: "STATISTIC_TYPE_COUNT",
|
|
278
|
+
sum: "STATISTIC_TYPE_SUM",
|
|
279
|
+
avg: "STATISTIC_TYPE_AVG",
|
|
280
|
+
min: "STATISTIC_TYPE_MIN",
|
|
281
|
+
max: "STATISTIC_TYPE_MAX",
|
|
282
|
+
stddev: "STATISTIC_TYPE_STDDEV",
|
|
283
|
+
variance: "STATISTIC_TYPE_VAR",
|
|
284
|
+
}[metric.fn];
|
|
285
|
+
const outputName = grpcOutputFieldName(metric.as, `$.metrics[${index}].as`);
|
|
286
|
+
const folded = outputName.toLowerCase();
|
|
287
|
+
if (outputNames.has(folded)) {
|
|
288
|
+
semanticUnsupported("unsupported-source", `$.metrics[${index}].as`, "QueryFeatures result fields must be unique under server case folding");
|
|
289
|
+
}
|
|
290
|
+
outputNames.add(folded);
|
|
291
|
+
return {
|
|
292
|
+
statisticType,
|
|
293
|
+
onStatisticField: grpcPublicFieldName(field, state, `$.metrics[${index}].field`),
|
|
294
|
+
outStatisticFieldName: outputName,
|
|
295
|
+
};
|
|
296
|
+
});
|
|
297
|
+
return {
|
|
298
|
+
outStatistics,
|
|
299
|
+
...(groupBy.length > 0 ? { groupBy } : {}),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function compileGrpcFilterParts(filter, state, path) {
|
|
303
|
+
if (filter.kind === "spatial")
|
|
304
|
+
return { spatial: { filter, path } };
|
|
305
|
+
if (filter.kind === "boolean" && filter.operator === "and") {
|
|
306
|
+
const where = [];
|
|
307
|
+
let spatial;
|
|
308
|
+
filter.args.forEach((entry, index) => {
|
|
309
|
+
const part = compileGrpcFilterParts(entry, state, `${path}.args[${index}]`);
|
|
310
|
+
if (part.where)
|
|
311
|
+
where.push(part.where);
|
|
312
|
+
if (part.spatial) {
|
|
313
|
+
if (spatial) {
|
|
314
|
+
semanticUnsupported("unsupported-node", part.spatial.path, "QueryFeatures carries only one spatial filter");
|
|
315
|
+
}
|
|
316
|
+
spatial = part.spatial;
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
return {
|
|
320
|
+
...(where.length > 0 ? { where: where.join(" AND ") } : {}),
|
|
321
|
+
...(spatial ? { spatial } : {}),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
if (containsGrpcSpatial(filter)) {
|
|
325
|
+
semanticUnsupported("unsupported-node", path, "QueryFeatures cannot preserve spatial predicates inside OR or NOT");
|
|
326
|
+
}
|
|
327
|
+
return { where: compileGrpcAttributeFilter(filter, state, path) };
|
|
328
|
+
}
|
|
329
|
+
function containsGrpcSpatial(filter) {
|
|
330
|
+
if (filter.kind === "spatial")
|
|
331
|
+
return true;
|
|
332
|
+
if (filter.kind === "boolean")
|
|
333
|
+
return filter.args.some((entry) => containsGrpcSpatial(entry));
|
|
334
|
+
if (filter.kind === "not")
|
|
335
|
+
return containsGrpcSpatial(filter.arg);
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
function compileGrpcAttributeFilter(filter, state, path) {
|
|
339
|
+
switch (filter.kind) {
|
|
340
|
+
case "comparison": {
|
|
341
|
+
const field = semanticSchemaField(state.schema, filter.left.name, `${path}.left.name`);
|
|
342
|
+
const operator = { eq: "=", ne: "<>", lt: "<", lte: "<=", gt: ">", gte: ">=" }[filter.operator];
|
|
343
|
+
return `${grpcWhereFieldName(field, state, `${path}.left.name`)} ${operator} ${grpcWhereLiteral(filter.right.value, field, `${path}.right.value`)}`;
|
|
344
|
+
}
|
|
345
|
+
case "list":
|
|
346
|
+
return semanticUnsupported("unsupported-node", path, "Honua gRPC where clauses do not support IN expressions");
|
|
347
|
+
case "range": {
|
|
348
|
+
const field = semanticSchemaField(state.schema, filter.operand.name, `${path}.operand.name`);
|
|
349
|
+
const name = grpcWhereFieldName(field, state, `${path}.operand.name`);
|
|
350
|
+
const lower = grpcWhereLiteral(filter.lower.value, field, `${path}.lower.value`);
|
|
351
|
+
const upper = grpcWhereLiteral(filter.upper.value, field, `${path}.upper.value`);
|
|
352
|
+
return `${name} >= ${lower} AND ${name} <= ${upper}`;
|
|
353
|
+
}
|
|
354
|
+
case "null": {
|
|
355
|
+
const field = semanticSchemaField(state.schema, filter.operand.name, `${path}.operand.name`);
|
|
356
|
+
return `${grpcWhereFieldName(field, state, `${path}.operand.name`)} IS ${filter.operator === "is-not-null" ? "NOT " : ""}NULL`;
|
|
357
|
+
}
|
|
358
|
+
case "pattern": {
|
|
359
|
+
const field = semanticSchemaField(state.schema, filter.operand.name, `${path}.operand.name`);
|
|
360
|
+
if (filter.caseSensitive === false) {
|
|
361
|
+
semanticUnsupported("unsupported-node", `${path}.caseSensitive`, "Honua gRPC where clauses cannot preserve case-insensitive LIKE semantics");
|
|
362
|
+
}
|
|
363
|
+
return `${grpcWhereFieldName(field, state, `${path}.operand.name`)} LIKE ${grpcQuotedString(filter.pattern, `${path}.pattern`)}`;
|
|
364
|
+
}
|
|
365
|
+
case "boolean": {
|
|
366
|
+
if (filter.operator !== "and") {
|
|
367
|
+
semanticUnsupported("unsupported-node", path, "Honua gRPC where clauses support only AND composition");
|
|
368
|
+
}
|
|
369
|
+
return filter.args
|
|
370
|
+
.map((entry, index) => compileGrpcAttributeFilter(entry, state, `${path}.args[${index}]`))
|
|
371
|
+
.join(" AND ");
|
|
372
|
+
}
|
|
373
|
+
case "not":
|
|
374
|
+
return semanticUnsupported("unsupported-node", path, "Honua gRPC where clauses do not support NOT composition");
|
|
375
|
+
case "temporal": {
|
|
376
|
+
const field = semanticSchemaField(state.schema, filter.operand.name, `${path}.operand.name`);
|
|
377
|
+
const name = grpcWhereFieldName(field, state, `${path}.operand.name`);
|
|
378
|
+
if (filter.operator === "before" || filter.operator === "after") {
|
|
379
|
+
return `${name} ${filter.operator === "before" ? "<" : ">"} ${grpcWhereLiteral(filter.value.value, field, `${path}.value.value`)}`;
|
|
380
|
+
}
|
|
381
|
+
const interval = filter.value.value;
|
|
382
|
+
const lower = grpcWhereLiteral(interval[0], field, `${path}.value.value[0]`);
|
|
383
|
+
const upper = grpcWhereLiteral(interval[1], field, `${path}.value.value[1]`);
|
|
384
|
+
return `${name} >= ${lower} AND ${name} <= ${upper}`;
|
|
385
|
+
}
|
|
386
|
+
case "native": {
|
|
387
|
+
const where = grpcNativeWhere(filter.payload.value, state, path);
|
|
388
|
+
state.usesNativeFilter = true;
|
|
389
|
+
return where;
|
|
390
|
+
}
|
|
391
|
+
case "spatial":
|
|
392
|
+
semanticUnsupported("unsupported-node", path, "Spatial filters require the QueryFeatures spatial_filter field");
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
function grpcNativeWhere(value, state, path) {
|
|
396
|
+
if (value === null || Array.isArray(value) || typeof value !== "object") {
|
|
397
|
+
semanticUnsupported("unsupported-native-filter", path, "honua-grpc native filter payload must be an object containing only where");
|
|
398
|
+
}
|
|
399
|
+
const keys = Object.keys(value);
|
|
400
|
+
const where = value.where;
|
|
401
|
+
if (keys.length !== 1 || keys[0] !== "where" || typeof where !== "string" || where.trim().length === 0) {
|
|
402
|
+
semanticUnsupported("unsupported-native-filter", path, "honua-grpc native filter payload must be an object containing one non-empty where string");
|
|
403
|
+
}
|
|
404
|
+
return verifiedGrpcNativeWhere(where, state, path);
|
|
405
|
+
}
|
|
406
|
+
function compileGrpcSpatial(filter, state, path) {
|
|
407
|
+
if (filter.operator === "bbox-intersects") {
|
|
408
|
+
semanticUnsupported("unsupported-geometry", path, "QueryFeatures has no envelope geometry message; bbox is not fabricated as a polygon");
|
|
409
|
+
}
|
|
410
|
+
if (filter.operator === "within-distance" || filter.operator === "beyond-distance") {
|
|
411
|
+
semanticUnsupported("unsupported-distance", `${path}.distance`, "QueryFeatures does not encode the semantic planar/geodesic distance mode");
|
|
412
|
+
}
|
|
413
|
+
const fieldName = filter.property?.name ?? primaryGeometryField(state.schema);
|
|
414
|
+
if (!fieldName) {
|
|
415
|
+
semanticUnsupported("unsupported-geometry", `${path}.property`, "QueryFeatures requires a primary geometry field");
|
|
416
|
+
}
|
|
417
|
+
const primary = primaryGeometryField(state.schema);
|
|
418
|
+
if (fieldName !== primary) {
|
|
419
|
+
semanticUnsupported("unsupported-geometry", `${path}.property`, "QueryFeatures cannot target a secondary geometry field");
|
|
420
|
+
}
|
|
421
|
+
const sourceGeometry = sourceGrpcGeometryField(state.schema, fieldName, path);
|
|
422
|
+
const sourceCrs = executableGrpcCrs(sourceGeometry.crs, `${path}.property`);
|
|
423
|
+
const literalCrs = filter.geometry.crs;
|
|
424
|
+
grpcSpatialReferenceFromBinding(sourceCrs, `${path}.property`);
|
|
425
|
+
const literalReference = grpcSpatialReferenceFromBinding(literalCrs, `${path}.geometry.crs`);
|
|
426
|
+
if (!sameExecutableCrs(sourceCrs, literalCrs)) {
|
|
427
|
+
semanticUnsupported("crs-transform-required", `${path}.geometry.crs`, "Spatial geometry CRS differs from the QueryFeatures source field");
|
|
428
|
+
}
|
|
429
|
+
const relationship = {
|
|
430
|
+
equals: "SPATIAL_RELATIONSHIP_EQUALS",
|
|
431
|
+
intersects: "SPATIAL_RELATIONSHIP_INTERSECTS",
|
|
432
|
+
within: "SPATIAL_RELATIONSHIP_WITHIN",
|
|
433
|
+
contains: "SPATIAL_RELATIONSHIP_CONTAINS",
|
|
434
|
+
disjoint: "SPATIAL_RELATIONSHIP_DISJOINT",
|
|
435
|
+
touches: "SPATIAL_RELATIONSHIP_TOUCHES",
|
|
436
|
+
overlaps: "SPATIAL_RELATIONSHIP_OVERLAPS",
|
|
437
|
+
crosses: "SPATIAL_RELATIONSHIP_CROSSES",
|
|
438
|
+
}[filter.operator];
|
|
439
|
+
return {
|
|
440
|
+
geometry: grpcGeometry(filter.geometry, path),
|
|
441
|
+
spatialRelationship: relationship,
|
|
442
|
+
spatialReference: literalReference,
|
|
443
|
+
crs: literalCrs,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
function grpcGeometry(value, path) {
|
|
447
|
+
assertGrpcCoordinateOrder(value.crs, `${path}.geometry.crs`);
|
|
448
|
+
if (value.layout !== "xy" && value.layout !== "xyz") {
|
|
449
|
+
semanticUnsupported("unsupported-geometry", `${path}.geometry.layout`, "QueryFeatures geometry cannot preserve measured coordinate layouts");
|
|
450
|
+
}
|
|
451
|
+
return grpcCanonicalGeometry(value.geometry, value.layout, path);
|
|
452
|
+
}
|
|
453
|
+
function grpcCanonicalGeometry(geometry, layout, path) {
|
|
454
|
+
switch (geometry.type) {
|
|
455
|
+
case "Point":
|
|
456
|
+
return { point: grpcCoordinate(geometry.coordinates, layout) };
|
|
457
|
+
case "MultiPoint":
|
|
458
|
+
return { multiPoint: { points: geometry.coordinates.map((position) => grpcCoordinate(position, layout)) } };
|
|
459
|
+
case "LineString":
|
|
460
|
+
return {
|
|
461
|
+
polyline: { paths: [{ coords: geometry.coordinates.map((position) => grpcCoordinate(position, layout)) }] },
|
|
462
|
+
};
|
|
463
|
+
case "MultiLineString":
|
|
464
|
+
return {
|
|
465
|
+
polyline: {
|
|
466
|
+
paths: geometry.coordinates.map((line) => ({
|
|
467
|
+
coords: line.map((position) => grpcCoordinate(position, layout)),
|
|
468
|
+
})),
|
|
469
|
+
},
|
|
470
|
+
};
|
|
471
|
+
case "Polygon":
|
|
472
|
+
return {
|
|
473
|
+
polygon: {
|
|
474
|
+
rings: geometry.coordinates.map((ring) => ({
|
|
475
|
+
coords: ring.map((position) => grpcCoordinate(position, layout)),
|
|
476
|
+
})),
|
|
477
|
+
},
|
|
478
|
+
};
|
|
479
|
+
case "MultiPolygon":
|
|
480
|
+
return {
|
|
481
|
+
multiPolygon: {
|
|
482
|
+
polygons: geometry.coordinates.map((polygon) => ({
|
|
483
|
+
rings: polygon.map((ring) => ({
|
|
484
|
+
coords: ring.map((position) => grpcCoordinate(position, layout)),
|
|
485
|
+
})),
|
|
486
|
+
})),
|
|
487
|
+
},
|
|
488
|
+
};
|
|
489
|
+
case "GeometryCollection":
|
|
490
|
+
semanticUnsupported("unsupported-geometry", `${path}.geometry.geometry`, "honua.v1.Geometry has no GeometryCollection payload");
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
function grpcCoordinate(position, layout) {
|
|
494
|
+
return {
|
|
495
|
+
x: position[0],
|
|
496
|
+
y: position[1],
|
|
497
|
+
...(layout === "xyz" ? { z: position[2] } : {}),
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
function executableGrpcCrs(value, path) {
|
|
501
|
+
try {
|
|
502
|
+
return validateExecutableCrsBinding(value, path);
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
semanticUnsupported("unsupported-crs", path, "QueryFeatures requires a resolved source CRS and payload order");
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
function grpcSpatialReferenceFromBinding(value, path) {
|
|
509
|
+
if (value.coordinateEpoch !== undefined) {
|
|
510
|
+
semanticUnsupported("unsupported-crs", path, "QueryFeatures spatial references cannot preserve coordinate epochs");
|
|
511
|
+
}
|
|
512
|
+
assertGrpcCoordinateOrder(value, path);
|
|
513
|
+
return grpcSpatialReference(value.definition, path);
|
|
514
|
+
}
|
|
515
|
+
function grpcSpatialReference(value, path) {
|
|
516
|
+
if (value.kind === "authority" && value.authority.toUpperCase() === "EPSG" && /^\d+$/.test(value.code)) {
|
|
517
|
+
const wkid = Number(value.code);
|
|
518
|
+
if (Number.isSafeInteger(wkid) && wkid > 0 && wkid <= 2_147_483_647)
|
|
519
|
+
return { wkid };
|
|
520
|
+
}
|
|
521
|
+
if (value.kind === "wkt" && value.validation === "engine")
|
|
522
|
+
return { wkt: value.wkt };
|
|
523
|
+
semanticUnsupported("unsupported-crs", path, "QueryFeatures spatial references require an EPSG code or engine-validated WKT");
|
|
524
|
+
}
|
|
525
|
+
function assertGrpcCoordinateOrder(value, path) {
|
|
526
|
+
const [x, y] = value.coordinateOrder.axes;
|
|
527
|
+
const xDirection = x?.direction;
|
|
528
|
+
const yDirection = y?.direction;
|
|
529
|
+
if (xDirection !== "east" || yDirection !== "north") {
|
|
530
|
+
semanticUnsupported("unsupported-crs", path, "QueryFeatures geometry requires east/north x/y payload axes");
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
function sourceGrpcGeometryField(schema, fieldName, path) {
|
|
534
|
+
if (schema.geometry.state !== "known") {
|
|
535
|
+
semanticUnsupported("unsupported-source", path, "Source geometry metadata is unavailable");
|
|
536
|
+
}
|
|
537
|
+
const field = schema.geometry.fields.find((candidate) => candidate.field === fieldName);
|
|
538
|
+
if (!field)
|
|
539
|
+
semanticUnsupported("unsupported-source", path, "Source primary geometry metadata is unavailable");
|
|
540
|
+
return field;
|
|
541
|
+
}
|
|
542
|
+
function primaryGeometryField(schema) {
|
|
543
|
+
return schema.geometry.state === "known" && schema.geometry.primaryField.state === "known"
|
|
544
|
+
? schema.geometry.primaryField.field
|
|
545
|
+
: undefined;
|
|
546
|
+
}
|
|
547
|
+
const GRPC_SERVER_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
548
|
+
const GRPC_SERVER_NUMBER = /^-?\d+(?:\.\d+)?$/;
|
|
549
|
+
const GRPC_SERVER_COMPARISON = /^([a-zA-Z_][a-zA-Z0-9_]*)\s*(NOT\s+LIKE|LIKE|>=|<=|!=|<>|=|>|<)\s*('(?:''|[^'])*'|-?\d+(?:\.\d+)?)$/i;
|
|
550
|
+
const GRPC_SERVER_NULL_CHECK = /^([a-zA-Z_][a-zA-Z0-9_]*)\s+IS\s+(?:NOT\s+)?NULL$/i;
|
|
551
|
+
const GRPC_SERVER_TRUE_LITERAL = /^(?:1\s*=\s*1|TRUE)$/i;
|
|
552
|
+
const DOTNET_DECIMAL_MAX = 79228162514264337593543950335n;
|
|
553
|
+
// FeatureQueryBuilder rejects these tokens before it parameterizes the legacy
|
|
554
|
+
// where grammar. Public fields with one of these exact names therefore cannot
|
|
555
|
+
// be represented consistently across the server's PostgreSQL readers.
|
|
556
|
+
const GRPC_SERVER_FORBIDDEN_WHERE_FIELDS = new Set([
|
|
557
|
+
"and",
|
|
558
|
+
"union",
|
|
559
|
+
"select",
|
|
560
|
+
"insert",
|
|
561
|
+
"update",
|
|
562
|
+
"delete",
|
|
563
|
+
"drop",
|
|
564
|
+
"create",
|
|
565
|
+
"alter",
|
|
566
|
+
"exec",
|
|
567
|
+
"execute",
|
|
568
|
+
"xp_",
|
|
569
|
+
"sp_",
|
|
570
|
+
"information_schema",
|
|
571
|
+
"copy",
|
|
572
|
+
"truncate",
|
|
573
|
+
"grant",
|
|
574
|
+
"revoke",
|
|
575
|
+
"vacuum",
|
|
576
|
+
"analyze",
|
|
577
|
+
"explain",
|
|
578
|
+
"do",
|
|
579
|
+
"raise",
|
|
580
|
+
"perform",
|
|
581
|
+
"notify",
|
|
582
|
+
"listen",
|
|
583
|
+
"pg_read_file",
|
|
584
|
+
"pg_write_file",
|
|
585
|
+
"pg_ls_dir",
|
|
586
|
+
"pg_stat_file",
|
|
587
|
+
"lo_import",
|
|
588
|
+
"lo_export",
|
|
589
|
+
"pg_sleep",
|
|
590
|
+
"current_setting",
|
|
591
|
+
"set_config",
|
|
592
|
+
"pg_advisory_lock",
|
|
593
|
+
"pg_advisory_unlock",
|
|
594
|
+
"dblink",
|
|
595
|
+
"dblink_connect",
|
|
596
|
+
"pg_terminate_backend",
|
|
597
|
+
"pg_cancel_backend",
|
|
598
|
+
"pg_reload_conf",
|
|
599
|
+
"query_to_xml",
|
|
600
|
+
"xpath",
|
|
601
|
+
"returning",
|
|
602
|
+
"chr",
|
|
603
|
+
"convert_from",
|
|
604
|
+
]);
|
|
605
|
+
function grpcWhereLiteral(value, field, path) {
|
|
606
|
+
if (value === null || Array.isArray(value) || typeof value === "object") {
|
|
607
|
+
semanticUnsupported("unsupported-field-type", path, `Honua gRPC does not encode ${field.type.kind} as a scalar where literal`);
|
|
608
|
+
}
|
|
609
|
+
switch (field.type.kind) {
|
|
610
|
+
case "boolean":
|
|
611
|
+
return semanticUnsupported("unsupported-field-type", path, "Honua gRPC server where clauses do not carry boolean literals portably");
|
|
612
|
+
case "integer":
|
|
613
|
+
if (typeof value === "number" || typeof value === "string")
|
|
614
|
+
return grpcServerNumber(value, path);
|
|
615
|
+
break;
|
|
616
|
+
case "float":
|
|
617
|
+
if (typeof value === "number")
|
|
618
|
+
return grpcServerNumber(value, path);
|
|
619
|
+
break;
|
|
620
|
+
case "decimal":
|
|
621
|
+
if (typeof value === "number" || typeof value === "string")
|
|
622
|
+
return grpcServerNumber(value, path);
|
|
623
|
+
break;
|
|
624
|
+
case "string":
|
|
625
|
+
case "uuid":
|
|
626
|
+
case "date":
|
|
627
|
+
case "time":
|
|
628
|
+
case "timestamp":
|
|
629
|
+
if (typeof value === "string")
|
|
630
|
+
return grpcQuotedString(value, path);
|
|
631
|
+
break;
|
|
632
|
+
case "duration":
|
|
633
|
+
case "binary":
|
|
634
|
+
case "json":
|
|
635
|
+
case "geometry":
|
|
636
|
+
case "list":
|
|
637
|
+
case "struct":
|
|
638
|
+
case "union":
|
|
639
|
+
case "unknown":
|
|
640
|
+
semanticUnsupported("unsupported-field-type", path, `Honua gRPC semantic filtering does not support logical type ${field.type.kind}`);
|
|
641
|
+
}
|
|
642
|
+
semanticUnsupported("unsupported-field-type", path, `Honua gRPC literal is incompatible with logical type ${field.type.kind}`);
|
|
643
|
+
}
|
|
644
|
+
function grpcServerNumber(value, path) {
|
|
645
|
+
const token = typeof value === "number" ? String(value) : value;
|
|
646
|
+
if (!GRPC_SERVER_NUMBER.test(token) || !grpcServerDecimalFits(token)) {
|
|
647
|
+
semanticUnsupported("unsupported-field-type", path, "Honua gRPC numeric literals must fit the server decimal grammar without exponent or sign prefixes");
|
|
648
|
+
}
|
|
649
|
+
return token;
|
|
650
|
+
}
|
|
651
|
+
function grpcServerDecimalFits(token) {
|
|
652
|
+
const unsigned = token.startsWith("-") ? token.slice(1) : token;
|
|
653
|
+
const [whole = "", fraction = ""] = unsigned.split(".");
|
|
654
|
+
const significantFraction = fraction.replace(/0+$/, "");
|
|
655
|
+
if (significantFraction.length > 28)
|
|
656
|
+
return false;
|
|
657
|
+
const coefficientText = `${whole}${significantFraction}`.replace(/^0+/, "") || "0";
|
|
658
|
+
if (coefficientText.length > 29)
|
|
659
|
+
return false;
|
|
660
|
+
return BigInt(coefficientText) <= DOTNET_DECIMAL_MAX;
|
|
661
|
+
}
|
|
662
|
+
function grpcQuotedString(value, path) {
|
|
663
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: mirrors server char.IsControl admission
|
|
664
|
+
if (/[\u0000-\u001f\u007f-\u009f]/.test(value)) {
|
|
665
|
+
semanticUnsupported("unsupported-field-type", path, "Honua gRPC where string literals cannot contain control characters");
|
|
666
|
+
}
|
|
667
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
668
|
+
}
|
|
669
|
+
function grpcPublicFieldName(field, state, path) {
|
|
670
|
+
if (!GRPC_SERVER_IDENTIFIER.test(field.name)) {
|
|
671
|
+
semanticUnsupported("unsupported-source", path, "QueryFeatures requires an ASCII public field name accepted by the server");
|
|
672
|
+
}
|
|
673
|
+
const folded = field.name.toLowerCase();
|
|
674
|
+
if (state.schema.fields.filter((candidate) => candidate.name.toLowerCase() === folded).length !== 1) {
|
|
675
|
+
semanticUnsupported("unsupported-source", path, "QueryFeatures cannot resolve case-insensitively ambiguous public field names");
|
|
676
|
+
}
|
|
677
|
+
return field.name;
|
|
678
|
+
}
|
|
679
|
+
function grpcWhereFieldName(field, state, path) {
|
|
680
|
+
const name = grpcPublicFieldName(field, state, path);
|
|
681
|
+
if (GRPC_SERVER_FORBIDDEN_WHERE_FIELDS.has(name.toLowerCase())) {
|
|
682
|
+
semanticUnsupported("unsupported-source", path, "QueryFeatures cannot represent this public field in server where grammar");
|
|
683
|
+
}
|
|
684
|
+
return name;
|
|
685
|
+
}
|
|
686
|
+
function grpcOutputFieldName(value, path) {
|
|
687
|
+
if (!GRPC_SERVER_IDENTIFIER.test(value)) {
|
|
688
|
+
semanticUnsupported("unsupported-source", path, "QueryFeatures statistic aliases require an ASCII server field identifier");
|
|
689
|
+
}
|
|
690
|
+
return value;
|
|
691
|
+
}
|
|
692
|
+
function verifiedGrpcNativeWhere(value, state, path) {
|
|
693
|
+
const wherePath = `${path}.payload.value.where`;
|
|
694
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: mirrors server char.IsControl admission
|
|
695
|
+
if (value.length > 4_000 || /[\u0000-\u001f\u007f-\u009f]/.test(value)) {
|
|
696
|
+
semanticUnsupported("unsupported-native-filter", wherePath, "honua-grpc native where text exceeds the server admission boundary");
|
|
697
|
+
}
|
|
698
|
+
const where = value.trim();
|
|
699
|
+
const clauses = splitGrpcServerAnd(where);
|
|
700
|
+
if (clauses.length === 1 && GRPC_SERVER_TRUE_LITERAL.test(clauses[0]?.trim() ?? ""))
|
|
701
|
+
return where;
|
|
702
|
+
if (clauses.length === 0) {
|
|
703
|
+
semanticUnsupported("unsupported-native-filter", wherePath, "honua-grpc native where text is not server-executable");
|
|
704
|
+
}
|
|
705
|
+
for (const clause of clauses) {
|
|
706
|
+
const trimmed = clause.trim();
|
|
707
|
+
const nullMatch = GRPC_SERVER_NULL_CHECK.exec(trimmed);
|
|
708
|
+
const comparisonMatch = GRPC_SERVER_COMPARISON.exec(trimmed);
|
|
709
|
+
const fieldName = nullMatch?.[1] ?? comparisonMatch?.[1];
|
|
710
|
+
if (!fieldName || GRPC_SERVER_TRUE_LITERAL.test(trimmed)) {
|
|
711
|
+
semanticUnsupported("unsupported-native-filter", wherePath, "honua-grpc native where text is not server-executable");
|
|
712
|
+
}
|
|
713
|
+
const field = state.schema.fields.find((candidate) => candidate.name === fieldName);
|
|
714
|
+
if (!field) {
|
|
715
|
+
semanticUnsupported("unsupported-native-filter", wherePath, "honua-grpc native where fields must use exact public schema names");
|
|
716
|
+
}
|
|
717
|
+
grpcWhereFieldName(field, state, wherePath);
|
|
718
|
+
const literal = comparisonMatch?.[3];
|
|
719
|
+
if (literal && !literal.startsWith("'") && !grpcServerDecimalFits(literal)) {
|
|
720
|
+
semanticUnsupported("unsupported-native-filter", wherePath, "honua-grpc native where text is not server-executable");
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
return where;
|
|
724
|
+
}
|
|
725
|
+
function splitGrpcServerAnd(value) {
|
|
726
|
+
const parts = [];
|
|
727
|
+
let current = "";
|
|
728
|
+
let inQuotes = false;
|
|
729
|
+
for (let index = 0; index < value.length; index++) {
|
|
730
|
+
const character = value[index];
|
|
731
|
+
if (character === "'") {
|
|
732
|
+
if (inQuotes && value[index + 1] === "'") {
|
|
733
|
+
current += "''";
|
|
734
|
+
index++;
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
inQuotes = !inQuotes;
|
|
738
|
+
current += character;
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
if (!inQuotes && value.slice(index, index + 3).toUpperCase() === "AND") {
|
|
742
|
+
const previous = index > 0 ? value[index - 1] : " ";
|
|
743
|
+
const next = index + 3 < value.length ? value[index + 3] : " ";
|
|
744
|
+
if (!grpcServerIdentifierCharacter(previous) && !grpcServerIdentifierCharacter(next)) {
|
|
745
|
+
parts.push(current);
|
|
746
|
+
current = "";
|
|
747
|
+
index += 2;
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
current += character;
|
|
752
|
+
}
|
|
753
|
+
// Preserve an empty final segment so a terminal separator cannot turn an
|
|
754
|
+
// invalid native expression into the preceding valid clause.
|
|
755
|
+
parts.push(current);
|
|
756
|
+
return parts;
|
|
757
|
+
}
|
|
758
|
+
function grpcServerIdentifierCharacter(value) {
|
|
759
|
+
return /[a-zA-Z0-9_]/.test(value);
|
|
760
|
+
}
|