# v0.87.15 - Versioned Row Identity V2 Contracts

> **Status:** Implemented
> **Scope:** 7 person-weeks
> **User promise:** *"Every row identity is deterministic, exact, and portable across supported PostgreSQL environments."*
> **Blocked by:** [v0.87.14](v0.87.14.md)
> **Proposal:** [Exact, Versioned Row Identity V2](../proposals/PROPOSAL_VERSIONED_ROW_IDENTIFY_ENCODING.md)

## Theme

Freeze the semantic and wire contracts for the V2 row identity before changing
storage or DVM operators. The release replaces display-text hashing as an
identity definition with typed PostgreSQL datum encoding, explicit type and
domain registries, and independently reproducible golden vectors. It delivers
the encoder and validation foundation, but does not yet change existing stream
table storage or consume V2 state in production refresh paths. A checked-in
normative wire-format artifact is a release deliverable, not an implementation
note, and later code must be tested against that artifact.

## Scope allocation

| Work package | Person-weeks |
|---|---:|
| Normative wire format and version/domain registry | 1.50 |
| Typed scalar and structural encoder dispatch | 2.00 |
| Type, collation, key-eligibility, and size validation | 1.25 |
| Golden vectors, cross-platform checks, and documentation | 1.25 |
| Review, failure-path tests, and release integration | 1.00 |
| **Total** | **7.00** |

## Items

### ROWID-1: Freeze the V2 wire format

Write and check in the normative byte-format specification consumed by the
implementation and tests. Assign identity version `2`, probe version `1`, and
stable values for identity domains, field type tags, NULL/value tags,
fixed-width transforms, escaped variable-width payloads, nested framing, tuple
termination, numeric normalization, the XXH3-128 seed, and digest byte order.
The artifact must be linked from the proposal and this roadmap, and a
spec-to-code vector check must fail when the implementation diverges. Once
released, changes to these assignments require a new identity version and a
rebuild rather than silently changing existing bytes.

**Dependencies:** v0.87.14 correctness oracle; proposal sections 3-5 and 8.
**Schema change:** No.

### ROWID-2: Add one extension-wide type and domain registry

Define registry entries by concrete type, typmod, collation, equality operator
family, nested metadata, and PostgreSQL major version. Each entry must declare
equality-canonical status, default non-NULL B-tree ordering, volatility, maximum
encoded size, supported majors, and DDL invalidations. Register the initial
scalar families and publish an initial structural support matrix that names
which arrays, ranges, multiranges, JSONB, composites, and other structural
families ship in V2; every family not marked supported is rejected at
validation. Explicitly reject types, collations, operator classes, and
structural forms whose equality or serialization contract is not proven.

**Dependencies:** ROWID-1; PostgreSQL catalog and operator-family contracts.
**Schema change:** No.

### ROWID-3: Implement typed datum encoding

Add the typed `encode_row_id_v2(domain, record) RETURNS bytea` entry point and a
single dispatch path that deforms PostgreSQL datums from the record descriptor.
Use stable semantic tags rather than runtime OIDs, preserve field order and
NULLs, canonicalize signed zero, NaN, numeric scale, the complete 128-bit
PostgreSQL interval comparison value, network values, and enum labels, and
recurse only through registered structural encoders. Resolve dispatch once per
record typmod with `fn_extra` and write to a reusable scratch buffer; never fall
back to `TEXT`, output functions, or a 64-bit hash.

**Dependencies:** ROWID-1 and ROWID-2.
**Schema change:** No.

### ROWID-4: Validate identity schemas before state creation

Validate every potential identity field at stream-table creation and defining
query change. Check equality semantics, source-key eligibility, collation and
operator class policy, nested support, volatility, maximum encoded size, DDL
invalidation coverage, and the running PostgreSQL major. Source-key validation
must accept only a primary key, or a non-partial, non-expression, immediate
unique index whose columns are all `NOT NULL` or whose constraint is `NULLS NOT
DISTINCT`; it must reject nullable `NULLS DISTINCT`, partial, expression,
deferrable, and other non-immediate unique forms. Return contextual errors
naming the expression, resolved type, and unsupported property; treat
uncertainty as rejection rather than silently selecting a weaker identity.

**Dependencies:** ROWID-2 and ROWID-3.
**Schema change:** No.

### ROWID-5: Publish reproducible identity vectors

Create an independent vector corpus covering every supported scalar family,
NULLs, composite framing, all ordering transforms, type modifiers, domains,
enums, deterministic and non-deterministic collations, nested values, malformed
or over-limit inputs, rejected source-index forms, the complete 128-bit interval
comparison value, and every identity domain. Verify exact bytes, equality
agreement, prefix freedom, and error classification on every supported
PostgreSQL major and CPU endianness. Include a spec-to-code check so the
independent vectors cannot silently drift from the normative artifact.
Keep vectors independent from the encoder implementation so a shared bug cannot
make the oracle agree with itself.

**Dependencies:** ROWID-1 through ROWID-4; v0.87.14 schema oracle.
**Schema change:** No.

## Correctness

| ID | Title | Effort | Priority |
|---|---|---:|:---:|
| CORR-1 | Prove equality and injectivity contracts | M | P0 |
| CORR-2 | Enforce prefix-free tuple framing | S | P0 |
| CORR-3 | Verify canonical special values | M | P0 |

**CORR-1** proves in both directions that PostgreSQL equality for each accepted
identity type agrees with byte equality, and that unequal values cannot silently
share a complete identity. Property tests compare the registry contract with
SQL equality across NULL, typmod, domain, enum, and supported nested cases.

**CORR-2** verifies field count, domain, type tags, variable-value escaping,
nested framing, and tuple termination. The corpus includes adversarial tuples
such as `(ab, c)` versus `(a, bc)` and complete identities where one payload
would otherwise be a prefix of another.

**CORR-3** locks down signed zero, NaN, infinities, numeric scale, interval
comparison normalization, `cidr` host bits, and enum label encoding. Any
semantic ambiguity produces a rejected input or a new explicit wire rule.

## Stability

| ID | Title | Effort | Priority |
|---|---|---:|:---:|
| STAB-1 | Bound encoder resource usage | M | P0 |
| STAB-2 | Classify unsupported inputs explicitly | S | P0 |
| STAB-3 | Preserve backend error safety | S | P1 |

**STAB-1** publishes concrete maximum nesting-depth and total encoded-size
limits, alongside typmod-aware bounds. Resource exhaustion raises a contextual
PostgreSQL error without switching algorithms, corrupting scratch state, or
leaving a partially returned varlena value visible.

**STAB-2** gives unsupported types, non-deterministic collations, unproven
operator classes, unsupported PostgreSQL majors, and invalid identity schemas
distinct errors with actionable remediation. No path uses an unchecked fallback
or accepts a type merely because PostgreSQL exposes an output function.

**STAB-3** exercises encoder errors, catalog lookup errors, malformed records,
and repeated calls in one backend. PostgreSQL error capture and `fn_extra`
rebuilds must leave the backend reusable and must not retain stale type metadata.

## Performance

| ID | Title | Effort | Priority |
|---|---|---:|:---:|
| PERF-1 | Make fixed-width encoding allocation-light | M | P0 |
| PERF-2 | Cache dispatch by record typmod | S | P0 |
| PERF-3 | Benchmark encoder hot paths | M | P1 |

**PERF-1** keeps fixed-width integer, UUID, date/time, and short composite paths
branch-light, writes directly into one scratch buffer, and copies only the
completed result into PostgreSQL-owned memory. The implementation records
allocation and throughput baselines for later storage comparisons.

**PERF-2** caches tuple descriptors and resolved field encoders per call site and
anonymous record typmod. It validates the current typmod and rebuilds the cache
when PostgreSQL invalidates the descriptor, without catalog queries or fmgr
lookups per row on the unchanged path.

**PERF-3** measures one integer, UUID, composite integers, short and long text,
keyless wide rows, arrays or JSONB where supported, and enum-heavy workloads.
The benchmark records throughput, allocations, error rates, and the cost of
syscache enum-label resolution.

## Scalability

| ID | Title | Effort | Priority |
|---|---|---:|:---:|
| SCAL-1 | Keep registry resolution bounded | S | P0 |
| SCAL-2 | Bound structural recursion | S | P0 |
| SCAL-3 | Exercise many identity call sites | M | P1 |

**SCAL-1** ensures registry lookup is keyed by the complete semantic descriptor
and does not grow with the number of rows processed. Invalidation and cache
replacement remain local to the affected type and call site.

**SCAL-2** applies one documented depth and size policy to arrays, ranges,
multiranges, JSONB, and composites. A deeply nested value fails predictably
rather than consuming unbounded backend memory.

**SCAL-3** runs the same encoder through source keys, keyless rows, group keys,
join keys, set-operation keys, window keys, and synthetic identities. The test
matrix checks that domains stay disjoint without adding per-domain algorithms.

## Ease of Use

| ID | Title | Effort | Priority |
|---|---|---:|:---:|
| UX-1 | Document identity support and rejection | S | P0 |
| UX-2 | Explain identity validation failures | S | P0 |
| UX-3 | State the opaque identity contract | XS | P1 |

**UX-1** adds a support matrix for scalar and structural types, collations,
operator classes, PostgreSQL majors, size bounds, volatility, and DDL
invalidations. It clearly distinguishes exact identity support from native
B-tree ordering support.

**UX-2** makes validation errors identify the stream table, expression, type,
collation, or operator class and explain the supported alternative. Preflight
and create-time diagnostics use the same classifications.

**UX-3** documents that `__pgt_row_id` is an opaque implementation value, not a
business identifier or user-generated key. Diagnostics use a short fingerprint
rather than exposing complete reversible identity bytes by default.

## Test Coverage

| ID | Title | Effort | Priority |
|---|---|---:|:---:|
| TEST-1 | Maintain independent golden vectors | M | P0 |
| TEST-2 | Property-test equality agreement | M | P0 |
| TEST-3 | Test catalog and cache invalidation | S | P1 |

**TEST-1** runs the vector corpus in unit and PostgreSQL-backed tiers and checks
byte-for-byte output, ordering, prefix freedom, and error outcomes. Vectors are
reviewable fixtures, not snapshots regenerated by the implementation under test.

**TEST-2** generates supported values and compares encoded-byte equality with
PostgreSQL equality and unequal-value separation. It includes composite and
nested values plus the explicit NULL policy for identity fields.

**TEST-3** warms encoder caches, changes record metadata, renames enum labels,
and retries from a second session. Tests verify syscache visibility and DDL
invalidation without introducing a custom enum generation counter. The same
fixture covers non-deterministic collation rejection and all accepted and
rejected source-index forms.

## Conflicts & Risks

- The wire format is an immutable compatibility contract. Any unresolved tag,
  byte-order, numeric, interval, enum, or structural rule blocks ROWID-3 and
  must be resolved before the next release.
- Stable enum-label encoding means `encode_row_id_v2` is `STABLE`, not
  `IMMUTABLE`; the later index probe function must remain separate and truly
  immutable.
- The release may implement encoder and validation code while leaving V1
  storage untouched. No test may imply that V1 and V2 state can be consumed in
  one graph.
- Structural families not explicitly proven remain rejected. Adding support
  later uses a new type tag and does not rewrite existing V2 identities.
- Encoder code is SQL-reachable and must contain no `unwrap()` or `panic!()`;
  unsafe PostgreSQL access requires precise `// SAFETY:` comments.

## Exit criteria

- [ ] Normative V2 wire-format and probe-version specifications are reviewed.
- [ ] A checked-in normative wire-format artifact is linked from the proposal,
  and independent vectors fail on any spec-to-code drift.
- [ ] Type and identity-domain registries reject unsupported contracts
      explicitly and list all accepted PostgreSQL majors.
- [ ] The initial structural support matrix explicitly names shipped and
  rejected arrays, ranges, multiranges, JSONB, composites, and other
  structural families.
- [ ] Typed encoding produces exact, prefix-free, deterministic bytes without
      `TEXT` conversion or a final hash.
- [ ] Numeric normalization and the complete 128-bit interval comparison value
  are covered by vectors and acceptance tests.
- [ ] Source-key validation covers primary keys, `NULLS NOT DISTINCT`, nullable
  `NULLS DISTINCT`, partial, expression, deferrable, and non-immediate forms.
- [ ] Resource limits, cache invalidation, and backend reuse tests pass.
- [ ] Golden vectors pass independently on every supported PostgreSQL major and
      CPU endianness.
- [ ] Unit, PostgreSQL, property, documentation, and full repository CI gates
      pass.
