# v0.87.16 - Versioned Row Identity V2 Engine Integration > **Status:** Planned > **Scope:** 12 person-weeks > **User promise:** *"The same exact row identity drives storage, capture, refresh, and matching."* > **Blocked by:** [v0.87.15](v0.87.15.md) > **Proposal:** [Exact, Versioned Row Identity V2](../proposals/PROPOSAL_VERSIONED_ROW_IDENTIFY_ENCODING.md) ## Theme Move the V2 identity contract through every internal relation and maintenance path. Stream-table storage, change buffers, temporary relations, trigger CDC, WAL CDC, full refresh, differential refresh, joins, aggregates, and propagation must carry the complete canonical `BYTEA` identity, while indexes use either a bounded direct key or a bounded probe plus exact recheck. Version columns and runtime guards make a mixed V1/V2 graph fail closed before it can apply a wrong change. This release changes the physical row-identity type and is intentionally a breaking pre-1.0 release. It does not attempt to convert existing V1 stream tables in place; the recreation workflow and external-consumer runbook land in v0.87.17. ## Scope allocation | Work package | Person-weeks | |---|---:| | Storage, catalog version state, and generated DDL | 2.00 | | Direct indexes, boundedness, and expression probes | 2.00 | | Trigger and WAL CDC producer integration | 1.75 | | DVM producers, transport, and exact DML matching | 3.00 | | Runtime compatibility and replication contracts | 1.25 | | PostgreSQL, upgrade, benchmark, and review work | 2.00 | | **Total** | **12.00** | ## Items ### ROWID-6: Replace identity storage with versioned `BYTEA` Change every storage, delta, temporary, CDC, and cache relation carrying a row identity to `__pgt_row_id BYTEA NOT NULL`. Add extension-wide `row_identity_version = 2` and `row_probe_version = 1` state to stream-table and change-buffer metadata, and update generated SQL, catalog inspection, cleanup, and repair paths. Runtime readers compare these values before consuming rows, including when a new shared library is installed before `ALTER EXTENSION ... UPDATE`. Transport relations carry only the complete identity; no generated probe column or parallel V1/V2 storage strategy is introduced. **Dependencies:** v0.87.15; proposal sections 3, 9, and 10. **Schema change:** Yes. ### ROWID-7: Add bounded direct and unbounded probe indexes Compute the maximum encoded identity size conservatively from the resolved type schema, typmods, collation, nesting, and running cluster `BLCKSZ`. Check `BTMaxItemSize` together with varlena and index-tuple overhead at every create, alter, and preflight boundary, and test every supported page-size build. Use a direct B-tree on the complete `BYTEA` whenever the complete datum is proven to fit; make it unique only when the validated identity schema proves one row per identity, never merely because an index or probe exists. For unbounded schemas, select and freeze the benchmarked probe prefix, implement immutable parallel-safe `row_probe_v1(bytea)`, and create a non-unique expression index with no heap probe column. **Dependencies:** ROWID-2 through ROWID-5; running PostgreSQL page-size limits. **Schema change:** Yes. ### ROWID-8: Route all CDC producers through V2 encoding Update row-level trigger CDC and WAL CDC to emit the same full V2 identity bytes from typed datums. A shared source buffer may feed multiple consumers, so no consumer-specific encoding option or downstream preference lookup is allowed. Capture version metadata atomically with the buffer row and reject a producer configuration that cannot validate the source identity schema. **Dependencies:** ROWID-3 and ROWID-6. **Schema change:** Yes. ### ROWID-9: Update all DVM identity producers Replace hard-coded `BIGINT` identity creation in full refresh, differential refresh, IMMEDIATE mode, joins, aggregates, set operations, windows, keyless rows, and synthetic singleton/dummy rows. Pass typed records to the single V2 encoder, preserve the existing `RowIdStrategy` and `RowIdSchema` field choices, and frame derived child identities under their registered domains. Planning must reject a unique unbounded identity for `IMMEDIATE` mode before any DML or capture state is created; the non-unique expression index is not an authority for uniqueness and this release does not add per-identity locking. **Dependencies:** ROWID-3, ROWID-6, and the v0.87.14 DVM contracts. **Schema change:** No. ### ROWID-10: Make matching exact for every index strategy For bounded identities, use full `BYTEA` equality and the direct B-tree; use `ON CONFLICT (__pgt_row_id)` only where the identity is genuinely unique. For unbounded or keyless identities, use non-unique direct or expression indexes as an accelerator and include complete row-ID equality in every UPDATE, DELETE, MERGE, and counted-delete predicate. For an unbounded expression index, every candidate predicate must contain both `row_probe_v1(target.__pgt_row_id) = row_probe_v1(delta.__pgt_row_id)` and `target.__pgt_row_id = delta.__pgt_row_id`; the counted-delete path must retain the existing multiplicity accounting rather than deleting by probe alone. Never use a probe or digest as proof of identity or as a uniqueness constraint. **Dependencies:** ROWID-7 and ROWID-9. **Schema change:** No. ### ROWID-11: Enforce V1/V2 and replication compatibility Make runtime readers reject mismatched row-identity or probe versions before consuming a row, including when a new shared library is installed before `ALTER EXTENSION ... UPDATE` runs. Apply the V2 replica-identity rules: bounded unique tables may use the direct unique identity index, while unbounded and keyless tables use `REPLICA IDENTITY FULL`; publication column lists include what PostgreSQL requires and never expose the expression probe as a schema column. The version constants and expected relation/catalog shape are defined in one compatibility contract so old binaries reject V2 state and V2 binaries reject leftover V1 state without guessing. **Dependencies:** ROWID-6, ROWID-7, and existing catalog/replication hooks. **Schema change:** Yes. ## Correctness | ID | Title | Effort | Priority | |---|---|---:|:---:| | CORR-4 | Unify all identity producers | L | P0 | | CORR-5 | Recheck complete identities | M | P0 | | CORR-6 | Preserve keyless multiplicity | M | P0 | | CORR-7 | Reject mixed identity versions | M | P0 | **CORR-4** runs trigger CDC, WAL CDC, FULL refresh, differential refresh, IMMEDIATE mode, and every DVM operator through shared typed encoding. The same logical fields must produce the same bytes regardless of the producer or refresh mode. **CORR-5** proves that direct-index and probe-index paths produce identical results under insert, update, delete, collision-shaped values, and common prefixes. A probe collision may increase candidates but must never change the matched complete row identity. **CORR-6** verifies that identical logical keyless rows still receive identical identities and remain independently counted. Non-unique index paths and counted-delete behavior must not collapse multiplicity. **CORR-7** injects stale buffer rows, stale temporary relations, old catalog versions, and mixed dependency graphs. Readers fail before mutation with a version-specific diagnostic rather than guessing or replaying incompatible state. ## Stability | ID | Title | Effort | Priority | |---|---|---:|:---:| | STAB-4 | Make catalog changes atomic | M | P0 | | STAB-5 | Fail closed before DML | M | P0 | | STAB-6 | Preserve replica identity safety | M | P0 | **STAB-4** applies identity and probe version metadata in the same transaction as storage and buffer definition changes. Partial index creation, cleanup, or catalog updates leave no relation that a later refresh could mistake for V2. **STAB-5** validates type contracts, index readiness, version compatibility, relation shape, and the unique/unbounded `IMMEDIATE` rule before reading or mutating a delta. Errors include the stream table, buffer, expected versions, actual versions, rejected identity property, and remediation. **STAB-6** tests publication, UPDATE, DELETE, replica identity, and subscriber apply behavior for bounded unique, bounded keyless, and unbounded tables. The expression probe never becomes a replica identity or a published generated column. ## Performance | ID | Title | Effort | Priority | |---|---|---:|:---:| | PERF-4 | Freeze a measured probe size | M | P0 | | PERF-5 | Compare physical lookup strategies | L | P0 | | PERF-6 | Keep CDC identity overhead bounded | M | P1 | **PERF-4** benchmarks 32, 64, 128, and 256-byte prefixes across short keys, ordered wide keys, random wide keys, and long common-prefix identities. Select a probe size only after checking the actual `BTMaxItemSize`, varlena overhead, and index-tuple overhead for each supported `BLCKSZ`; the selected prefix and XXH3-128 algorithm/seed become the immutable probe-version-1 contract. **PERF-5** compares direct full-ID B-trees, expression probe indexes, generated probe columns, and PostgreSQL hash indexes for applicable workloads. Measure index size, buffer hits, WAL volume, cached and cold MERGE latency, and behavior when the index exceeds `shared_buffers`; generated columns and hash indexes are benchmarks, not required shipped strategies. **PERF-6** measures typed encoding, trigger and WAL capture, change-buffer storage, and refresh overhead against the V1 baseline. The common fixed-width path must retain the encoder cache and scratch-buffer properties established in v0.87.15. ## Scalability | ID | Title | Effort | Priority | |---|---|---:|:---:| | SCAL-4 | Share one source identity safely | M | P0 | | SCAL-5 | Scale unbounded identity matching | M | P0 | | SCAL-6 | Bound wide-row maintenance memory | M | P1 | **SCAL-4** feeds several downstream stream tables from one source buffer and checks that each consumer applies its own boundedness and probe classification without changing the source identity bytes. **SCAL-5** exercises long and common-prefix identities with non-unique expression indexes, exact rechecks, concurrent refreshes, and indexes larger than memory. It records candidate amplification and confirms that performance tradeoffs do not alter results. **SCAL-6** runs keyless wide rows, nested supported values, large deltas, and multi-consumer buffers under the existing memory budgets. Buffer cleanup and DVM transport must not create a second full-ID representation or unbounded probe state. ## Ease of Use | ID | Title | Effort | Priority | |---|---|---:|:---:| | UX-4 | Explain direct versus probe indexing | S | P1 | | UX-5 | Document replication consequences | S | P0 | | UX-6 | Remove obsolete V1 assumptions | M | P0 | **UX-4** reports whether a stream table uses a direct full-ID index or an expression probe, the selected probe version, and the reason an identity was classified as unbounded. It does not display complete identity bytes by default. **UX-5** documents the replica-identity and publication behavior for each table class, including the extra old-row cost of `REPLICA IDENTITY FULL` on unbounded and keyless UPDATE/DELETE workloads. **UX-6** updates generated SQL, SQL reference, diagnostics, examples, and operator-facing names so users do not infer that `BIGINT`, a generated probe column, or a probe uniqueness guarantee still exists. ## Test Coverage | ID | Title | Effort | Priority | |---|---|---:|:---:| | TEST-4 | Run the producer matrix | L | P0 | | TEST-5 | Test index equivalence | M | P0 | | TEST-6 | Verify replication matrix | M | P0 | | TEST-7 | Exercise version guards | M | P0 | **TEST-4** covers trigger CDC, WAL CDC, full refresh, differential refresh, IMMEDIATE mode, joins, aggregates, set operations, windows, and synthetic identities with the shared vectors from v0.87.15. **TEST-5** compares direct and probe-index plans against the exact schema oracle for inserts, updates, deletes, common-prefix identities, forced probe collisions, keyless duplicates, and zero-change refreshes. It asserts both byte-identical identities across producer paths and the complete probe-plus-ID predicate in generated UPDATE, DELETE, MERGE, and counted-delete SQL. **TEST-6** uses PostgreSQL publication and subscriber fixtures for bounded unique, bounded non-unique, unbounded, and keyless tables. It checks old-row availability, column lists, subscriber `BYTEA` storage, and absence of a published generated probe. **TEST-7** tests old binaries, new binaries before extension update, mixed buffers, stale temporary relations, and partial catalog metadata. All mismatch cases fail before consuming or mutating state. ## Conflicts & Risks - This release changes the physical type of `__pgt_row_id` and all dependent relations. Existing stream-table state is not converted; recreation belongs to v0.87.17. - Unique unbounded identities remain unsupported for `IMMEDIATE` mode. The non-unique expression index cannot enforce uniqueness, and this release must not introduce per-identity locking as a workaround. - Direct-index size classification is cluster-specific. A bound safe for one `BLCKSZ` must not be reused blindly on another build. - Expression probes are not identity values, replica identities, or publication columns. Every correctness predicate must still compare complete `BYTEA` IDs. - DVM engine changes require property tests and the v0.87.14 correctness gate; performance work cannot relax fail-closed behavior or durability. ## Exit criteria - [ ] All identity-bearing relations use `BYTEA NOT NULL` and carry compatible version metadata where required. - [ ] Identity version `2` and probe version `1` are enforced at runtime, with old/new binary mismatch tests before any row is consumed or mutated. - [ ] Trigger CDC, WAL CDC, refresh, DVM operators, and propagation use one V2 typed encoder. - [ ] Direct and expression index paths are selected from conservative, cluster-specific `BLCKSZ`/`BTMaxItemSize` bounds and use exact full-ID matching; the unique-index rule is tied to validated identity semantics. - [ ] Every unbounded UPDATE, DELETE, MERGE, and counted-delete predicate includes probe equality plus complete-ID equality, and unique unbounded `IMMEDIATE` identities are rejected during planning. - [ ] Version mismatches fail before DML, and no V1/V2 graph can be consumed. - [ ] Replication behavior is tested for every bounded, unbounded, and keyless table class. - [ ] Unit, PostgreSQL, integration, benchmark, upgrade-guard, and full repository CI gates pass.