Skip to content

patch: full table vocabulary + patch_view / patch_dictionary#156

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

patch: full table vocabulary + patch_view / patch_dictionary#156
orian merged 1 commit into
mainfrom
patch-vocabulary

Conversation

@orian

@orian orian commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements #154 — all five asks. After #153, 70 objects in posthog-cloud-infra were still copied per env, each blocked on a field no patch could carry (~30 on engine alone). The patch vocabulary is now complete:

patch_table carries the full delta:

patch_table "raw_sessions" {
  engine "distributed" { cluster_name = "posthog", ... }  # replace wholesale
  order_by     = [...]                                    # replace when set
  ttl          = "..."                                    # replace when set
  column "x" { ... }                                      # add
  modify_column "y" { ... }                               # in-place replace
  drop_columns = ["z"]
  index "i" { ... }                                       # add
  drop_indexes = ["j"]                                    # drop first → drop+add redefines
  settings = { ... }                                      # merge, patch wins (as before)
}
  • Column order within a patch: modify → drop → add (an add sees the post-drop state; modify_column keeps position).
  • engine replaces wholesale — per the issue, merging engine sub-arguments is not meaningful. The Distributed-target-moves-with-topology case is now one engine block per env instead of a 20-line copy.
  • Still non-patchable by design: primary_key, comment, constraints, projections — a table differing there is genuinely different, which is override = true's job.

patch_view (query, comment) and patch_dictionary (source/layout/lifetime replace wholesale, settings merge) give the last two object kinds a patch form. Unknown targets error, like patch_table.

Correctness details:

Plan doc: docs/plans/2026-07-17-patch-vocabulary.md. Docs: reference (patch_table rewritten, new patch_view/patch_dictionary section, pipeline + decision table), FAQ (engine question now "yes", two new worked entries: Distributed-per-env and view/dictionary patching), README, CLAUDE.md.

Note: this branch is stacked on #155 (the extend/patch_table docs explainer) so both PRs edit the same doc sections without conflicts. Merge #155 first and this diff reduces to the vocabulary work — or merge this one and #155's content lands with it.

Test plan

  • patch_vocabulary_test.go: the issue's raw_sessions engine scenario through LoadLayers + a cross-env diff that is exactly the engine change; scalar replace-when-set; column modify/drop/add ordering + all error paths; index drop+add redefine + errors; patch_view heredoc/one-liner convergence, comment replace, unknown target; patch_dictionary source/lifetime replace + settings merge + unknown target; locate exemption
  • Repurposed TestParseFile_PatchDisallowedAttribute (engine is legal now; primary_key stands in for the still-rejected fields)
  • go test -race ./internal/... ./cmd/... ./test — 985 passed; gofmt -s clean
  • Smoke-tested the built binary: composed dev layer carries the patched engine + normalized patched query; diff -left cloud -right cloud,dev reports exactly the engine change and query change

After #153, 70 objects in posthog-cloud-infra remained copied per env,
each blocked on a field no patch could carry — ~30 on engine alone
(Distributed targets that move with the env's topology), 6 on views and
dictionaries with no patch form at all, the rest on index / order_by /
ttl / column modification.

patch_table now carries the full delta vocabulary. Columns: add (must
not exist), modify_column (in-place replace, position kept, must
exist), drop_columns (must exist) — applied modify, drop, add, so an
add sees the post-drop state. Indexes: add and drop_indexes, drops
first, so a drop+add pair redefines an index in one patch. order_by /
partition_by / sample_by / ttl replace the target's value when set. The
engine block replaces wholesale — merging engine sub-arguments is not
meaningful. Settings keep their merge-patch-wins semantics.
primary_key, comment, constraints, and projections stay non-patchable:
a table differing there is genuinely different, which is override's
job.

patch_view (query, comment) and patch_dictionary (source / layout /
lifetime replace wholesale, settings merge) extend the same idea to the
object kinds that had no patch form. Both error on unknown targets.

Patched values canonicalize exactly like declared ones — patch column
and index expressions through the same normalization, patch_view
queries through normalizeQuery, patch engine and dictionary
source/layout decoded at parse — so a patched object converges with its
introspected form. The locate scanner records patch_view /
patch_dictionary as patch sites, keeping the -duplicates audit correct.

Closes #154
@orian
orian force-pushed the patch-vocabulary branch from f807f4d to 0ce86bd Compare July 18, 2026 09:34
@orian
orian enabled auto-merge (squash) July 18, 2026 09:36
@orian
orian merged commit adfbef4 into main Jul 18, 2026
16 checks passed
@orian
orian deleted the patch-vocabulary branch July 18, 2026 09:37
orian added a commit that referenced this pull request Jul 18, 2026
## 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.

```hcl
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 #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

- [x] `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)
- [x] `go test -race ./internal/... ./cmd/... ./test` — all green;
`gofmt -s` clean
- [x] Binary smoke test: composed interleaved order, `cmp`
byte-identical to the flat declaration, declared-column misuse rejected
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