Skip to content

patch_table: positioned column adds (after / first)#159

Merged
orian merged 1 commit into
mainfrom
patch-column-position
Jul 18, 2026
Merged

patch_table: positioned column adds (after / first)#159
orian merged 1 commit into
mainfrom
patch-column-position

Conversation

@orian

@orian orian commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements #158: a patch_table column 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.

patch_table "person" {
  column "pmat_email" {
    type  = "String"
    after = "is_deleted"   # or first = true
  }
  column "pmat_name" {
    type  = "String"
    after = "pmat_email"   # adds chain in patch order
  }
}

Semantics (both asks from the issue):

  • after = "<name>" inserts immediately after the named column; first = true inserts at the front; neither keeps today's append. Both together error.
  • Adds apply in patch order, each resolving against the post-previous-op state (the modify → drop → add discipline from patch: full table vocabulary + patch_view / patch_dictionary #156): after may name a column added earlier in the same patch; a dropped or unknown target errors.
  • Canonicalization: placement is transient (cleared on application, diff:"-" like renamed_from) — the composed table renders byte-identical to the same table declared plainly in that order. The acceptance test asserts Write(base+patch) == Write(flat), and the binary smoke test confirms it with cmp.
  • Patch-only, by design: after/first on a declared table column (including inherited from an abstract base), on an MV column, or on modify_column is 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/AFTER in generated DDL) stays open — this is compose-side only, and the transient placement fields on ColumnSpec are the shared model plumbing #88 can build on.

Plan doc: docs/plans/2026-07-18-patch-column-position.md; docs updated in the patch_table reference (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) equals Write(flat)); chained after; first + append mix with cleared placement; after against post-modify/drop state; every error path (both set, unknown/dropped target, on modify_column, on a declared column, inherited from an abstract base, on an MV column)
  • go test -race ./internal/... ./cmd/... ./test — all green; gofmt -s clean
  • Binary smoke test: composed interleaved order, cmp byte-identical to the flat declaration, declared-column misuse rejected

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
@orian
orian merged commit 620b9a7 into main Jul 18, 2026
16 checks passed
@orian
orian deleted the patch-column-position branch July 18, 2026 15:13
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
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