Skip to content

feat: 0.5.0 payload - CONCURRENTLY by default on both surfaces + statement_timeout - #6

Merged
juanmicl merged 8 commits into
mainfrom
feat/0.5.0-concurrently
Sep 2, 2026
Merged

feat: 0.5.0 payload - CONCURRENTLY by default on both surfaces + statement_timeout#6
juanmicl merged 8 commits into
mainfrom
feat/0.5.0-concurrently

Conversation

@juanmicl

@juanmicl juanmicl commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Summary

The 0.5.0 payload: CONCURRENTLY by default on both surfaces, plus statement_timeout — implementing the adjudicated design (12 decisions, oracle-authored; one mid-cycle design repair, see below).

Push surface

  • CONCURRENTLY-by-default rendering for existing-table indexes in plan/push/revision (one injection point, final pipeline stage after both dedups — the byte-containment dedup invariant holds by construction; new-table indexes stay in-transaction plain; create_hypertable ops unaffected). revision-generated chain files inherit the same rendering.
  • Opt-out: concurrently=False (API) / --no-concurrently (push, revision) — the documented INVALID-index recovery escape. Threaded through the advisory-lock winner re-plan (the pinned hazard, test-pinned).
  • concurrent boolean on every operation in the v1 plan JSON — additive only, golden contract updated.
  • Session GUCs on the concurrent segment: lock_timeout now bounds CREATE INDEX CONCURRENTLY too (session SET + RESET with invalidate-on-failed-reset hygiene), not just the transactional segment.
  • statement_timeout on push (SET LOCAL in the txn segment, session+RESET on autocommit; default None = server default untouched; negative rejected up front).

Chain surface

  • Hybrid replay: files without CONCURRENTLY keep the byte-identical 0.4.2 whole-text fast path (dollar-quote safe, zero tokenization). Files containing CONCURRENTLY replay per-op on the -- op N [label] delimiters (the chain-spec §7 mechanism): plain ops first in one transaction, concurrent ops statement-by-statement on a dedicated autocommit connection, versions row last — first concurrent failure blocks the file (partial_failure, no versions row, honest note when the plain segment already committed).
  • statement_timeout on migrate (SET LOCAL per file, session GUC on the autocommit connection, --statement-timeout).
  • Design repair (oracle-endorsed): PG executes a multi-statement simple-protocol string as one implicit transaction even on autocommit connections — so the concurrent lane splits statements with a psql-equivalent boundary scanner (dollar-quotes, ''/"" doubling, comments; confined to that lane only). Survived a 25-case adversarial audit with no silent mis-split; deviations only on exotic input, all fail-loud.

Docs

  • CHANGELOG [Unreleased]: 6 Added bullets (API + CLI named) + a Known-limitations section (hand-edit tokenization cost per chain spec §7, INVALID-index recovery, mixed-file crash window, fail-loud re-runs). README aligned (behavior, JSON additive field, roadmap); stale asyncpg roadmap line dropped (shipped in 0.4.2).

Verification

  • 166 passed, 1 xfailed (baseline 144 + 22 new) against live timescaledb-ha:pg17; DB-free subset skips cleanly
  • ruff check, ruff format --check, ty check all clean
  • Every lane TDD with observed-red pre-implementation (exceptions documented honestly: mutation-guard tests and one anti-simplification pin)
  • Review chain: oracle design adjudication → 3 implementation lanes → full-diff oracle review — verdict approve for 0.5.0 (4 nits; the one actionable nit — stale README roadmap line — folded in as fe70525)

Release surface

Exit codes unchanged. This PR is the 0.5.0 payload: on merge, cut v0.5.0.

PlannedOperation gains a frozen-dataclass field `concurrent: bool = False`
and to_json_dict emits it per operation. Additive to the JSON v1 contract:
the schema's required list and the golden payload gain the key; existing
keys/values are byte-stable. Defaults keep every hand-built-plan
construction site (tests, users) unchanged.
Final pipeline stage _render_concurrent (after _dedup_enum_types, before
Plan) rewrites standalone add_index ops on tables NOT created in the same
plan to CREATE [UNIQUE] INDEX CONCURRENTLY via an anchored, idempotent,
count=1 regex, setting the new concurrent flag via dataclasses.replace.
The ordering invariant (dedup never sees a CONCURRENTLY render, so the
byte-containment match holds by construction) is commented in plan().
DiffEngine.plan gains concurrently=True to skip injection entirely.

Executor _is_concurrent becomes a union: op.concurrent (authoritative
for generated plans) OR the SQL substring (hand-built Plans). classify
docstring updated: add_index stays RISKY in both renderings.
A7: _session_gucs contextmanager SETs session-level GUCs on the
autocommit segment's connection and RESETs each in finally; a reset
failure is suppressed and the connection invalidated so the pool never
hands out a session with a shrunken budget (0.4.2 lesson). The segment
now runs with the same lock_timeout as the txn segment — previously it
had no lock budget at all.

B12: apply_plan gains statement_timeout (None = no GUC anywhere);
not-None reaches the txn segment as SET LOCAL alongside lock_timeout
(inline-ms, no bind params) and the concurrent segment via _session_gucs
+ RESET. Negative values are rejected up front, mirroring the 0.4.2
budget-validation pattern.

Tests: concurrent-segment lock budget + pooled RESET observability,
statement_timeout reaching both segments (current_setting probes) with
None touching no GUC, negative validation, and the live end-to-end
push (valid index in pg_indexes, honest partial_failure).
…sion

api.plan/push/revision gain concurrently=True; api.push gains
statement_timeout=None. THREADING HAZARD fixed: both knobs flow
api.push -> with_advisory_lock -> the lock winner's reverify.plan(...)
AND the final apply_plan — without it the winner re-planned with the
default rendering, silently ignoring the caller's request. Pinned by
test_concurrently_opt_out_end_to_end (spy on the re-plan kwargs + the
captured plan's plain SQL + live apply).

CLI: --no-concurrently on push and revision, --statement-timeout on
push, following the existing flag patterns.
Fast path (no CONCURRENTLY in the file's text) stays byte-identical
to 0.4.2: whole text, one txn, one exec_driver_sql call — the parser
never executes, so dollar-quoted bodies (and their internal -- lines)
are safe. Files containing CONCURRENTLY replay per-op on the op-label
delimiters: plain segment first in one txn (create-to-index deps
inside one file), concurrent ops on a dedicated lazily-created
autocommit connection (B11: one per walk, session lock_timeout via
_session_gucs, RESET + invalidate-then-close via ExitStack), versions
row only after all concurrent ops succeed. A failed concurrent op
blocks the file: partial_failure, no versions row, strict-order stop,
and an honest note when the plain segment already committed.

Implementation discovery (live-verified against the dev DB): the
server executes a multi-statement simple-protocol string as ONE
implicit transaction block even on an autocommit connection, so
CREATE INDEX CONCURRENTLY cannot ride a multi-statement
exec_driver_sql call. The concurrent lane therefore executes
statement-per-execute via _split_statements — a dollar-quote,
string-literal and comment aware boundary scanner (psql-equivalent).
Generated ops are single statements (splitter is a no-op); only
hand-edited label-less bodies exercise it, matching the spec §7
at-the-author's-risk stance. Plain segment and fast path never split.
Module docstring rewritten to the hybrid contract (B9).
run_migrate gains statement_timeout (seconds, None = set nothing):
SET LOCAL in every per-file transaction (fast path and the mixed
path's plain segment) and a session statement_timeout on the
CONCURRENTLY autocommit connection — set once at connection creation
via _session_gucs alongside lock_timeout, RESET before the connection
closes. Negative values are rejected up front with a typed
SqlpushError, before any connection or file work (0.4.2 validation
pattern). Plumbed through api.migrate(statement_timeout=None) and the
migrate verb's --statement-timeout flag, following push's flag.
@juanmicl
juanmicl merged commit f0c4eca into main Sep 2, 2026
7 checks passed
@juanmicl
juanmicl deleted the feat/0.5.0-concurrently branch September 2, 2026 20:28
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