Skip to content

fix(persistence): preserve tstzrange endpoint inclusivity in membership overlap admission #618

Open

Description

@seonghobae

Finding

After #617 closes the unbounded-range NULL bypass, the #616 storage trigger still compares only scalar lower()/upper() timestamps with <=. That loses the inclusivity metadata carried by PostgreSQL tstzrange uncertainty windows.

0006_typed_membership_assignment permits non-empty ranges with inclusive or exclusive endpoints. The share-budget rule is intentionally conservative only when two memberships can overlap. Two uncertainty envelopes that merely meet at timestamp t cannot overlap when at least one relevant endpoint excludes t, but the current scalar predicate treats equality as overlap unconditionally.

Example: an existing membership has valid_to_window = '[2026-04-10,2026-04-11)' and a candidate has valid_from_window = '[2026-04-11,2026-04-11]'. The existing end can never reach 2026-04-11, so the spells are disjoint. The current upper(existing.valid_to_window) == lower(candidate.valid_from_window) comparison nevertheless rejects a second full share.

This is a buyer/scientific correctness issue in the opposite direction from #617: fail-closed storage must not silently collapse valid time-varying disjoint spells into a false overlap.

Required RED

On #615 or its verified successor, add canonical embedded PostgreSQL 18 contracts proving:

  • excluded existing upper endpoint + included candidate lower endpoint at the same timestamp is disjoint and remains admissible;
  • included existing upper endpoint + included candidate lower endpoint at the same timestamp is a real possible overlap and rejects an over-budget write;
  • included existing upper endpoint + excluded candidate lower endpoint at the same timestamp is disjoint and remains admissible;
  • fix(persistence): close unbounded temporal-window bypass in membership share budget #617 upper/lower-unbounded cases remain fail closed when overlap is possible.

Causal repair boundary

Preserve PostgreSQL range semantics directly. Prefer constructing the possible-activity envelope from the earliest admissible start and latest admissible end while retaining endpoint inclusivity, then use range-overlap semantics (&&) rather than timestamp nudging, epsilon/tolerance, or ad hoc microsecond arithmetic. Open-ended and unbounded uncertainty must remain supported.

Do not change the exact binary64 share arithmetic, tenant/observed-unit/role guard serialization, row-local (0,1] constraint, or historical migrations.

Acceptance

Keep open until public RED -> minimal owner-consistent repair, exact-head live PostgreSQL evidence, coverage/security/review, prerequisite landing/restack, and downstream #604 recovery are complete.

Refs #604, #615, #616, #617.

Activity

  1. seonghobae commented on Sep 19, 2026

    @seonghobae
    ContributorAuthor

    #615 now carries the endpoint-semantics repair ordinary-forward.

    • Public live PostgreSQL RED: bed68e3aceb49170a5a984622940159f22c26456
      • excluded existing upper + included candidate lower at the same instant stays disjoint;
      • included/included meeting remains a real overlap and rejects an over-budget write;
      • included existing upper + excluded candidate lower stays disjoint.
    • Minimal causal repair: ebd816eb9d8153d54905273aefe05ae7846fc767
      • constructs each row's possible-activity tstzrange from earliest admissible start to latest admissible end;
      • preserves unbounded sides and endpoint inclusivity;
      • uses PostgreSQL range overlap && instead of scalar <=, timestamp nudging, or tolerance;
      • leaves exact binary64 share arithmetic and guard-row serialization unchanged.

    Fresh exact-head hosted evidence is still pending, so #618 remains open and no GREEN/merge claim is made.

  2. seonghobae commented on Sep 19, 2026

    @seonghobae
    ContributorAuthor

    Current surviving #615 head is now def43f135a75d6d20ed51ee918b902ede0ddf8cf. The only delta after causal repair ebd816eb... is compile/API doctoring: the new live-test helper originally declared Result<u64, PersistenceError> while canonical SqlSession::execute returns Result<(), PersistenceError>; def43f13... aligns the test helper to that existing API without changing production semantics. Fresh exact-head Rust Foundation 35461497979 is queued with all four jobs still pre-checkout, so no GREEN claim or blind rerun.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions