Source schema v2 migration
Source schema v2 is an experimental, vendor-neutral discovery projection. It
is available from the focused @honua/sdk-js/source-schema subpath and can
enrich GeoServices, OData 4.0, and GeoParquet discovery. The legacy schema and
the default lightweight connect() path remain unchanged during this rollout.
Opt in without issuing a second metadata request:
import { connectWithSourceSchemaV2 } from "@honua/sdk-js/source-schema";
const connection = await connectWithSourceSchemaV2({
endpoint,
protocol: "odata",
authorizationScopeFingerprint: "anonymous",
});
The focused connection uses a distinct cache identity and validates cached v2
fingerprints before use. Plain root connect() neither emits nor trusts an
injected schemaV2; this keeps the schema runtime and pinned PROJJSON validator
out of root, /honua, browser, query-planner, and ordinary /contract bundles.
Its focused return type exposes the complete validated schema. Generic
SourceDescriptor and discovery-cache types expose only the lightweight
kind, version, and fingerprint transport envelope; pass such a value to
parseSourceSchemaV2 before inspecting schema semantics.
Compatibility window
Existing code can continue to read source.descriptor.schema:
const legacy = source.descriptor.schema;
const objectId = legacy?.primaryKey;
const esriType = legacy?.fields?.[0]?.type;
That object still contains HonuaFieldInfo values and Esri field-type strings.
It is not used as the source of truth for new semantic identity. New code can
opt into the parallel projection:
const schema = source.descriptor.schemaV2;
if (schema) {
const idFields = schema.fields.filter((field) => field.roles.includes("feature-id"));
const geometryFields = schema.fields.filter((field) => field.roles.includes("geometry"));
console.log(schema.version, schema.fingerprint, idFields, geometryFields);
}
Do not translate LogicalType values back into Esri strings. Applications that
need raw GeoServices metadata should use the existing HonuaClient metadata
methods or a protocol-native handle. A logical unknown value is intentional:
its bounded native reference records what was observed without pretending it
was a string, point, or another portable type.
Field mapping
| Legacy access | Schema v2 access |
|---|---|
field.type === "esriFieldTypeOID" |
field.roles contains primary-key or feature-id |
field.type === "esriFieldTypeGeometry" |
field.roles contains geometry; inspect schema.geometry for kind, layout, CRS, and primary-field state |
schema.primaryKey |
schema.key (known, none, or evidence-bearing unknown) |
schema.timeField |
schema.temporal and the time-* field roles |
field.length |
field.type.kind === "string" && field.type.maxLength |
| absent/unknown native type coerced to string | field.type.kind === "unknown" with a bounded native reference |
Every v2 field explicitly distinguishes nullability, mutability, value-domain,
and constraint knowledge. none means metadata affirmatively reported an
unconstrained field or the concept cannot apply to that logical type;
unknown means metadata was absent, not inspected in this migration slice,
invalid, conflicting, unrecognized, or over the safety bound. In particular,
missing GeoServices domain, unparsed OData validation annotations, and
uninspected GeoParquet/Arrow constraints are unknown/not-reported, not
invented declarations of no constraint. An explicit GeoServices domain: null
remains none/unconstrained.
A known key is executable identity, not a name hint: every member must be a
non-nullable scalar whose JSON encoding can produce FeatureIdentityValue.
OData key properties therefore become non-nullable when the CSDL Nullable
facet is omitted; an explicit nullable contradiction fails projection.
GeoServices infers a key only from one unique OID field. Multiple OID
candidates, a missing declared objectIdField, or a declaration that targets a
non-OID field becomes unknown/conflicting and carries no primary-key role.
Scalar date and timestamp types do not by themselves establish a dataset time
dimension. In this rollout only explicit GeoServices timeInfo assigns
time-* roles or a non-none schema.temporal; ordinary OData and GeoParquet
date columns remain ordinary fields. OData non-flags enums are logical strings
whose coded values are member names, matching their JSON wire form. Their
underlying numeric declarations remain bounded native evidence rather than
portable values. Mixed implicit/explicit declarations are evaluated in CSDL
document order (first omitted value 0, later omitted value previous-plus-one)
and fail closed on overflow, invalid names, or an empty declaration.
duration remains a supported scalar amount but cannot be used as a temporal
instant or interval endpoint. A range whose endpoints are equal is valid only
when both endpoints are inclusive; either exclusive endpoint would describe an
empty domain and is rejected.
OData metadata parsed by the SDK records projection-safety evidence without
changing its legacy string-keyed shape. The focused projector rejects a
selected entity shape when namespace-local type names are ambiguous or when
the entity or a referenced complex type uses unresolved BaseType
inheritance. It never labels a partial inherited shape openContent: "closed".
CRS values distinguish definition-axis order from payload coordinate order.
OData's omitted SRID defaults are represented explicitly: Geography uses
EPSG:4326, while Geometry uses the protocol's SRID 0 / unspecified Cartesian
binding without fabricating EPSG:0. GeoParquet PROJJSON is checked against a
pinned, generated validator for the official v0.7 CRS schema. Invalid CRS
metadata degrades that binding to unknown without discarding the field or
geometry inventory.
GeoServices WKT is retained only when it has a recognized WKT1/WKT2 CRS root,
a quoted name, and balanced, fully closed delimiters. It is marked
validation: "unverified" and is not an executable CRS until an engine-backed
validator promotes it; arbitrary strings remain bounded native evidence under
an unknown/unrecognized definition. Reprojected bindings also require their
public definition to match the normalized reprojection target.
Non-null geometry defaults use exact canonical RFC 7946 shapes. All positions must have one ordinate arity, polygon rings must close, and the value's root type and arity must agree with declared geometry-type and layout knowledge.
GeoParquet 1.0 references PROJJSON v0.5, while this public normalized contract
intentionally pins v0.7. A valid 1.0 CRS carrying the v0.5 schema identifier is
therefore retained as bounded native evidence and exposed as an unknown CRS
definition unless it can be promoted safely. Its WKB payload order remains
known x/y; inability to promote CRS semantics does not erase encoding facts.
Identity, caches, and plans
schema.fingerprint is SHA-256 over the accepted semantic projection from the
source-contract v2 decision. Presentation text, observation timestamps, and raw
native definitions do not change it; logical type, roles, domains,
constraints, key, geometry/CRS, temporal semantics, and extensions do.
The connection discovery snapshot remains v4. The focused projector adds its
own cache-identity suffix, stores schemaV2 only under that distinct identity,
and revalidates the full v2 fingerprint on every cache read. Default
connect() cache identities and snapshots are unchanged.
Generic query-planner entry points do not consume descriptor.schemaV2 yet.
They cannot distinguish a focused, validated value from an arbitrary object on
a caller-constructed SourceDescriptor without importing the full validator.
Until descriptor-native plan identity lands, use the focused validating bridge
to feed the verified fingerprint through the existing planner context:
import { executeQueryPlan, explainQuery } from "@honua/sdk-js/query-planner";
import { sourceSchemaV2QueryContext } from "@honua/sdk-js/source-schema";
const source = connection.source();
const schemaContext = sourceSchemaV2QueryContext(source.descriptor);
const plan = explainQuery({ descriptor: source.descriptor, query, ...schemaContext });
await executeQueryPlan(plan, source, schemaContext);
The helper reparses the full schema, so a forged or drifted fingerprint fails
before it can become cache identity. A changed validated schema changes plan
identity and old-plan execution fails with plan-context-mismatch. Full
descriptor, capability, and schema plan identity remains owned by the planned
descriptor-identity work rather than trusting generic transport envelopes.
Use serializeSourceSchemaV2, parseSourceSchemaV2, and
cloneSourceSchemaV2 for JSON-safe boundaries. They validate the discriminator,
version, canonical fingerprint, bounds, and nested values, and return a deeply
frozen schema. cloneSourceSchemaV2 performs the same full reparse and returns
a fully validated clone. Do not persist a plain object after manually changing
its fingerprint.
JSON parsing, structuredClone, worker transfer, and another installed copy of
the SDK preserve the structural SourceSchemaV2 value but do not by themselves
recompute its semantic fingerprint. Re-enter the focused runtime with
parseSourceSchemaV2 whenever such a value becomes authoritative (for example,
at an application cache or worker ingestion boundary). The focused connection
path does this on every discovery-cache hit. Lightweight generic discovery
checks only the kind, version, and fingerprint syntax needed to transport
the optional envelope. It does not maintain a hidden process-local trust
registry or claim to perform full schema validation; generic query planning
ignores the envelope entirely.
Bounded native definitions are diagnostic evidence, not a credential store. Serialization recursively redacts credential fields, URL user-info, signed query parameters, bearer/basic credentials, private keys, and high-confidence token literals while preserving ordinary protocol metadata and environment placeholders.
Rollout boundary
Only the opt-in GeoServices, OData 4.0, and GeoParquet discovery projection
populates schemaV2 in this slice. Other protocols and default root connect()
continue to expose their existing discovery shape until their scheduled
migration issues land. Missing schemaV2 means “not projected”; it must not be
replaced with an empty schema or a fabricated fingerprint.
On the opt-in path, missing schemaV2 is reserved for a protocol that did not
advertise a field inventory. Invalid advertised metadata or an internal
normalization failure rejects connectWithSourceSchemaV2() instead of being
silently converted to absence. Default connect() remains independent of this
experimental projection.