patch_table: positioned column adds (after / first)#159
Merged
Conversation
A patch could append or replace in place but not insert at a position, so an env whose extra columns interleave mid-table (posthog.person: ~96 pmat_* columns between is_deleted and version) could not be expressed as base+patch — physical column order matters for SELECT *, positional INSERT, and dump parity, so 12 objects (the largest tables among them) stayed fully redeclared. A patch column add now takes after = "<name>" (insert immediately after the named column) or first = true (insert at the front); neither keeps the append default, and both together error. Adds apply in patch order against the post-previous-op state — after may name a column added earlier in the same patch, and naming a dropped or unknown column errors — the modify → drop → add discipline unchanged. Placement is patch-only, transient metadata (cleared on application, diff:"-", like renamed_from): the composed table is byte-identical to the same table declared plainly in that order, so goldens converge with introspected dumps. A declared table or MV column carrying after/first errors at resolve — including via abstract-base inheritance — and modify_column rejects them: repositioning an existing column is genuine drift and stays a full redeclaration, per the issue. Related: #88 (ADD COLUMN FIRST/AFTER in generated DDL) stays open; the transient placement fields are the model plumbing it can reuse. Closes #158
This was referenced Jul 18, 2026
orian
added a commit
that referenced
this pull request
Jul 18, 2026
## Summary Implements #160 — the index analogue of #159, unblocking `posthog.sharded_events` (the last of the 12 order-blocked objects; ~380 columns, per-env `minmax_mat_*` skip indexes interleaved mid-list). ```hcl patch_table "sharded_events" { index "minmax_mat_b" { expr = "mat_b" type = "minmax" granularity = 1 after = "minmax_mat_a" # or first = true } } ``` Same semantics as #159's column placement, per the issue: - `after`/`first` on a patch `index` add; mutually exclusive; neither keeps the append default. - Post-previous-op resolution: drops apply first, so `after` may name an index added earlier in the same patch and a dropped/unknown target errors. A positioned drop+add **redefines an index at a position** — drop+add is already the sanctioned redefine path, and a redefine lands where you put it. - Placement is transient (cleared on application, `diff:"-"`): the composed table renders **byte-identical** to the flat declaration — asserted by test and confirmed with `cmp` against the built binary. - Patch-only: a declared table index carrying placement is a resolve error, including when inherited from an abstract base. Repositioning base indexes stays a full redeclaration. The column and index paths now share one `insertPos` helper (column error messages unchanged — #159's tests pass untouched). Plan doc: `docs/plans/2026-07-18-patch-index-position.md`; docs updated in the `patch_table` reference (index bullet + example), README, CLAUDE.md. ## Test plan - [x] The issue's `sharded_events` scenario in miniature with byte-parity (`Write(base+patch)` equals `Write(flat)`); `first` + chained `after` + positioned drop+add redefine in one patch with cleared placement; error paths (both set, unknown target, target dropped in the same patch, declared-index placement, abstract-base inheritance) - [x] `go test -race ./internal/... ./cmd/... ./test` — all green; `gofmt -s` clean - [x] Binary smoke test: composed interleaved index order, `cmp` byte-identical to the flat declaration
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements #158: a
patch_tablecolumn add can now be positioned, so an env whose extra columns interleave mid-table no longer forces a full redeclaration — the last large blocker (12 objects, the ~380-column tables among them) for the once-only rule in posthog-cloud-infra.Semantics (both asks from the issue):
after = "<name>"inserts immediately after the named column;first = trueinserts at the front; neither keeps today's append. Both together error.aftermay name a column added earlier in the same patch; a dropped or unknown target errors.diff:"-"likerenamed_from) — the composed table renders byte-identical to the same table declared plainly in that order. The acceptance test assertsWrite(base+patch) == Write(flat), and the binary smoke test confirms it withcmp.after/firston a declared table column (including inherited from an abstract base), on an MV column, or onmodify_columnis a resolve error — repositioning base columns is genuine drift and stays a full redeclaration, exactly as the issue scoped it.Related: #88 (emitting
ADD COLUMN … FIRST/AFTERin generated DDL) stays open — this is compose-side only, and the transient placement fields onColumnSpecare the shared model plumbing #88 can build on.Plan doc:
docs/plans/2026-07-18-patch-column-position.md; docs updated in thepatch_tablereference (column bullet + example), FAQ (positioned form added to the env-column recipe), README, CLAUDE.md.Test plan
patch_position_test.go: the issue's person scenario with byte-parity (Write(base+patch)equalsWrite(flat)); chainedafter;first+ append mix with cleared placement;afteragainst post-modify/drop state; every error path (both set, unknown/dropped target, onmodify_column, on a declared column, inherited from an abstract base, on an MV column)go test -race ./internal/... ./cmd/... ./test— all green;gofmt -scleancmpbyte-identical to the flat declaration, declared-column misuse rejected