Skip to content

transactions: improve update, support versioned feature mutations#525

Open
cportele wants to merge 7 commits into
masterfrom
tx-2
Open

transactions: improve update, support versioned feature mutations#525
cportele wants to merge 7 commits into
masterfrom
tx-2

Conversation

@cportele

@cportele cportele commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Property-level partial updates

Adds FeatureTransactions.PropertyUpdate and Session.patchFeature with a default unsupported implementation. SqlMutationSession implements it as native SQL on the session's open connection:

  • Updates VALUE and GEOMETRY columns in the main feature table
  • VALUE_ARRAY and OBJECT_ARRAY junction tables: DELETE existing rows for the feature and INSERT new rows
  • OBJECT_ARRAY elements with nested OBJECT children, M:N junctions and FEATURE_REF(_ARRAY) are unsupported and are rejected with an error.

Session API for versioned-features mutations

Adds the Session-level extensions the executor needs to drive retire-and-insert (Replace), retire-in-place / clone-and-patch (Update), and retire-only (Delete) flows on versioned collections.

SqlMutationSession implements these as hand-built UPDATE / SELECT statements on the session's JDBC connection so they share the atomic transaction with the executor's pre-existing patch/insert flows. retireFeature and patchOpenVersion accept an optional expectedStart predicate (If-Unmodified-Since-style guard for composite-id flows). createFeatures gains a post-INSERT pass that emits a follow-up UPDATE for role-bound columns the encoder can't reach because the property is scoped read+filter but not write (e.g. denorm predecessor pointers populated by the session, not by clients).

Two new SchemaBase roles, PREDECESSOR_INTERVAL_START and SUCCESSOR_INTERVAL_START, let versioned collections declare their denorm pointer columns to the same lookup machinery the existing interval roles use.

Adds FeatureTransactions.PropertyUpdate and Session.patchFeature with a
default unsupported implementation. SqlMutationSession implements it as
native SQL on the session's open connection so partial updates see prior
writes from the same transaction:

  - main-table SET for scalar/datetime/boolean columns
  - geometry via the existing toWkt path: GeoJSON in -> Geometry ->
    ST_GeomFromText (with ST_ForcePolygonCW for POLYGON / MULTI_POLYGON),
    matching the encoding the INSERT path emits
  - VALUE_ARRAY and OBJECT_ARRAY junctions: DELETE existing rows by
    parent_pk + INSERT new rows on the same session connection

OBJECT_ARRAY elements with nested OBJECT children, M:N junctions and
FEATURE_REF arrays remain unsupported and are rejected with a clear error.
Adds the Session-level extensions the executor needs to drive
retire-and-insert (Replace), retire-in-place / clone-and-patch
(Update), and retire-only (Delete) flows on versioned collections:

  - createFeatures(featureType, sources, crs, roleOverrides)
  - retireFeature(featureType, id, ts[, expectedStart])
  - patchOpenVersion(featureType, id, updates, idFilter[, expectedStart])
  - assertNoConflictingVersion(featureType, id, ts)
  - getOpenVersionStart(featureType, id)

SqlMutationSession implements these as hand-built UPDATE / SELECT
statements on the session's JDBC connection so they share the atomic
transaction with the executor's pre-existing patch/insert flows.
retireFeature and patchOpenVersion accept an optional expectedStart
predicate (If-Unmodified-Since-style guard for composite-id flows).
createFeatures gains a post-INSERT pass that emits a follow-up UPDATE
for role-bound columns the encoder can't reach because the property
is scoped read+filter but not write (e.g. denorm predecessor pointers
populated by the session, not by clients).

Two new SchemaBase roles, PREDECESSOR_INTERVAL_START and
SUCCESSOR_INTERVAL_START, let versioned collections declare their
denorm pointer columns to the same lookup machinery the existing
interval roles use.
@cportele cportele changed the title transactions: property-level partial update transactions: improve update, support versioned feature mutations Jun 7, 2026
cportele added 5 commits June 9, 2026 09:29
Each FeatureSchema property now carries an optional originObjectType:
the objectType of the schema fragment that originally listed the property.
LocalSchemaFragmentResolver tags every property contributed by a merged
fragment with the fragment's objectType (recursively, with outer-fragment
tagging winning over inner). The tag is @JsonIgnore + @DocIgnore: it is
populated only by schema resolution, not from YAML.

FeatureTokenDecoderGml's namespace-expectation chain now reads the
property's own originObjectType first, then — only when no origin is set
and the parent is a NESTED object (not the feature root) — falls back to
the parent's objectType. The feature root's objectType no longer
propagates down to property children, which is what lets a feature in a
domain namespace nest standard properties inherited from a base fragment
in the application's default namespace without dragging the feature's
prefix onto them.

Existing nested-object behaviour is unchanged: a property declared inline
under, say, an ISO 19115 metadata object still inherits the nested
object's namespace via the parent walk.
The versioned-insert pre-flight is now a plain id-existence check
(SELECT 1 FROM main WHERE idCol = ? LIMIT 1). The previous
three-predicate SQL silently allowed Insert on a retired feature id.
Clients add new versions through Replace / Update / Delete; Insert is
reserved for brand-new ids.

cloneAndPatchFeature ships (was throwing UnsupportedOperationException):
capture the open row's PK + start, clone the main row with inline
overrides (start = ts, end = NULL, predecessor, successor) and
main-table scalar patches, clone each junction table's rows redirecting
the FK to the new PK, retire the old row with the same startCol < ts
guard as retireFeature, then apply junction-backed patches via the
existing patchInternal path. An expectedStart overload threads the
composite-id If-Unmodified-Since predicate through; empty OLD result
surfaces as 409 or 412.

Specs cover the new SQL shape and fail-fast contracts.
- SchemaBase.Role.getLinkRelation() with PREDECESSOR_INTERVAL_START and
  SUCCESSOR_INTERVAL_START mapped to the predecessor-version /
  successor-version link relations.
- FeatureTokenTransformerLinkRoles strips role-as-link values from the
  token stream and surfaces them via Result.getRoleLinks() / context.
- FeatureTokenTransformerVersionIntervals captures the
  (PRIMARY_INTERVAL_START, PRIMARY_INTERVAL_END) tuples per feature for
  the Time Map endpoint.
- FeatureTokenTransformerExtension SPI lets a FeatureQueryExtension
  contribute a token-stream transformer; FeatureStreamImpl wires
  contributed transformers in the pre-format slot alongside LinkRoles.
- FeatureEventHandler ModifiableContext gains roleLinks() and
  canonicalFeatureId() (with the mirroring setters on
  FeatureEventHandlerSimple).
- FeatureStream Result and ResultReduced expose getRoleLinks() and
  getVersionIntervals() so the queries handler can build HTTP Link
  headers without re-decoding the response.
- FeatureTokenTransformerMappings propagates the per-feature context
  state (roleLinks, canonicalFeatureId) into its newContext before
  flushing the buffer; without that propagation upstream transformer
  state was dropped at the format-transformation boundary.
- DeterminePipelineStepsThatCannotBeSkipped keeps MAPPING_VALUES when a
  schema property carries a versioned-features role (ID,
  PRIMARY_INTERVAL_START/END, or a role that declares a link relation)
  so the default DATETIME_FORMAT transformer runs and the captured
  timestamps reach the new transformers in ISO 8601.
…-id bounds

SqlQueryTemplatesDeriver only treated a bare In(_ID_, …) as id-bounded
and computed a separate surrogate-key range guard otherwise. When a
single-feature query carries an extra predicate (e.g. TIntersects for
the datetime parameter), the filter becomes And(In, TIntersects) and
the id-filter check missed it, so meta-skip mode emitted
"A.id >= 0 AND A.id <= 0" — a closed empty range — and returned no
rows. Recurse into And's args so the id-list short-circuits even when
combined with other predicates.
The LinkRoles transformer fed every feature's role values into the result
builder via `putAllRoleLinks`, which is backed by `ImmutableMap.Builder`.
For a multi-version single-feature stream, repeated keys
(`predecessor-version` etc.) collided at `.build()` time and the whole
query aborted with `IllegalArgumentException: Multiple entries with same
key …` — the response handler then surfaced a generic 404.

Switch the setter to `roleLinks(map)` (replace) and choose explicitly
which feature's roles drive the result-level map: the one with the
greatest `PRIMARY_INTERVAL_START`, i.e. the latest version. For
non-versioned single-feature responses (no start) the only feature wins.
The result-level emission is gated on `context.metadata().isSingleFeature()`
so list responses no longer pollute `Result.getRoleLinks()` with
arbitrary per-feature data. Per-feature roles on the context
(`context.setRoleLinks`) — what writers use for per-feature link items —
are unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant