Skip to content

feat: aippatch — proto/SQL PATCH framework for AIP-134 RPCs#177

Open
btc wants to merge 77 commits into
mainfrom
worktree-aippatch-impl
Open

feat: aippatch — proto/SQL PATCH framework for AIP-134 RPCs#177
btc wants to merge 77 commits into
mainfrom
worktree-aippatch-impl

Conversation

@btc
Copy link
Copy Markdown
Owner

@btc btc commented May 9, 2026

Summary

Adds aippatch, a Go framework that turns AIP-134 PATCH RPCs into safe, dynamic Postgres UPDATE statements via codegen. The proto message and FieldMask carry intent on the wire; an aippatch.yaml plus a sibling code generator produce a typed Mapping[T] per resource; a runtime Apply[T] call validates the mask, builds the SQL, executes it, and returns the post-update proto.

  • Runtime library at thirdparty/aippatch/ — generic over proto.Message, no drill imports, ready to lift into a standalone Go module. Codecs: scalars, timestamps, enums; AutoSet for always-set columns (updated_at = NOW()); soft-delete and Op.Where scope predicates; pgx.Tx participation. Spec divergence from AIP-134 documented (empty mask rejected with InvalidArgument by default).
  • Codegen binary at thirdparty/aippatch/cmd/aippatchgen/ — reads buf.binpb + SQL migrations (via pg_query_go/v6) + aippatch.yaml; emits committed internal/patches/*.gen.go. --check mode for CI drift detection.
  • Drill integrationaippatch.yaml declares the User resource; internal/patches/{user,init}.gen.go is generated and committed; cmd/drill/main.go calls patches.InitPatches() at startup; UpdateProfile handler shrinks from ~45 lines to ~14 and uses aippatch.Apply; superseded UpdateUserDisplayName sqlc query and b.UpdateDisplayName removed.

The design and plan went through extensive review rounds before implementation; the implementation itself was executed task-by-task with two-stage review per task, then a final 3-round opus + sonnet review against the whole branch. Spec, plan, and review trail are all in docs/superpowers/.

Test plan

  • make test passes (buf lint, codegen drift check, frontend, backend -race, golangci-lint).
  • go test ./thirdparty/aippatch/... ./internal/rpc/user/... -race passes.
  • go run ./thirdparty/aippatch/cmd/aippatchgen --check exits 0.
  • Existing TestUpdateProfile_* handler tests pass unchanged (they assert via connect.CodeOf(err), not message text).
  • Manual: hit UpdateProfile against a running drill instance with a valid display_name patch; confirm response carries the post-update User and updated_at advances.

Notable behavior change

UpdateProfile now distinguishes zero-row outcomes (PK mismatch, soft-delete, future scope filters) from real DB errors: zero-row → NotFound, pgx errors → Internal. Previously every failure path of b.UpdateDisplayName collapsed to Internal. Documented in the spec's Wire conformance note.

Roadmap

v1+ items not in this PR (deliberately deferred): JSONB codec, nullable bound columns, declarative validators, AIP-193 error mapping, ETag concurrency, audit-log hooks. Each is backwards-compatible with v0 call sites.

btc added 30 commits May 7, 2026 21:52
A small Go library plus a sibling code generator that turn AIP-134 PATCH
RPCs into safe, dynamic Postgres UPDATE statements. The proto message and
FieldMask carry intent on the wire; an aippatch.yaml plus codegen produce
a typed Mapping[T] per resource; a runtime Apply[T] call validates the
mask, builds the SQL, executes it, and returns the post-update proto.

Built first at drill/thirdparty/aippatch/ with UpdateProfile as the v0
target; designed for clean extraction into a standalone module reusable
across Spanda LLC products. Codegen consumes proto FileDescriptorSet plus
parsed sql/migrations via pg_query_go plus aippatch.yaml policy; output
is committed *.gen.go files reviewable in PRs and guarded by a --check
mode in CI.

v0 codec set covers scalars, timestamps, and enums — sufficient to retire
drill's hand-rolled UpdateProfile handler. JSONB, declarative validators,
declarative authz, AIP-193 error mapping, and AIP-154 ETag are roadmap
items; each is backwards-compatible with v0 call sites.
Auto-cancel a Pro subscription via cancel_at_period_end=true when the
user has been idle for two consecutive billing periods. No settings
toggle — this is the default Sabermatic behavior. Establishes a Spanda
tenet: earn money for ongoing value delivered.

Trigger fires at Stripe invoice.upcoming (~T-7) when MAX(last_active)
across auth_sessions falls before the previous period's start, with
launch-day grandfathering via users.idle_eligible_after. Reversal via
email-link click (signed token, single-use) or auto-reverse on any
authenticated activity during the cancel window. sub_cancel_is_auto
distinguishes our auto-cancels from user-initiated portal cancels.

Migrations 015–018: auth_sessions soft-delete, users sub-state cache,
stripe_webhook_dedup, keep_link_token_uses. Feature package home at
internal/feat/idleunsub/.
Both opus and sonnet review agents returned ~30 findings. Major changes:

HIGH
- Acknowledge AIP-134 empty-mask deviation explicitly (Wire conformance
  note); ErrorOnEmpty stays as v0 default.
- Fix s.b.Pool() — it is a method, not a field.
- Drop "matches sqlc's DBTX" claim; document it is a strict subset
  satisfied by *pgxpool.Pool, *pgx.Conn, and pgx.Tx.
- Remove mustValidate panic helper from generated file; show clean
  Mapping literal plus generated InitPatches() error per drill's
  no-panic-at-init rule.
- Replace ub.SQL("RETURNING *") with ub.Returning(boundCols...) —
  first-class API, sends only mapped columns, excludes unmapped sensitive
  columns from the wire.
- Add AutoSet mechanism (yaml auto_set: { updated_at: NOW() }) so PATCH
  bumps updated_at like the existing sqlc UPDATEs do; without this every
  PATCH would leave updated_at stale.
- Use proto.CloneOf instead of proto.Clone(...).(T); add nil op.Message
  guard.
- Add buf build -o buf.binpb to make codegen — current buf generate does
  not emit a descriptor set.

MEDIUM
- Add Transactions subsection: Apply takes any DBTX including pgx.Tx.
- Add audit-logging hooks (returned diff) to v1 roadmap.
- Document CGO requirement for aippatchgen explicitly.
- Add NOT NULL extraction to migration replay; nullable columns deferred
  to v1.
- Define module-extraction trigger as "second project consumes aippatch
  in production" (v0.5).
- Audit existing test wording in rollout plan.
- Split smallint into separate type-table row with int16-to-int32
  widening on decode.
- Add validated bool to Mapping; Apply rejects unvalidated mappings with
  Internal.

LOW
- Document v0 nested-mask-path constraint and emit codegen diagnostic.
- List github.com/google/uuid in runtime imports.
- Add bytes/float/double to deferred-types table.
- State codecs are global and reusable across resources.
- Make algorithm step explicit that unmatched proto fields without
  skip:true are diagnostic.
- Add go get step to rollout plan.
- Note RETURNING uses bound-columns list (privacy benefit).
- Correct codec key description (yaml name, not protoreflect name).
- Add grep-for-callers step before deleting UpdateDisplayName.

NIT
- Use op.Mask.GetPaths() throughout.
- Clarify two-phase ordering (field-number processing, alphabetical emit).
- Spell out "foreign-key constraints".
- Document yaml string-to-Go-enum mapping for empty_mask.
- Diagram label uses thirdparty/aippatch/cmd/aippatchgen.
- Spanda replication wording: runtime/codegen are drill-free, not the yaml.

Spec grew from 690 to ~770 lines.
Round 2 surfaced one structural bug (m.codecs referenced but missing from
Mapping struct), pg_query_go v5/v6 drift, several real correctness issues,
and several smaller items. Fixes:

HIGH
- Add Codecs map[string]EnumCodec plumbing: shared Codecs registry in
  generated init.gen.go, passed into Mapping.Validate(codecs) which copies
  the relevant subset into unexported m.codecs. Apply now references
  m.codecs that actually exists.
- Add EnumCodec type to public API (ProtoEnum, ToText, FromText).
- Scrub pg_query_go to v6 throughout (latest published; v6.2.2).
- Tone down Returning(boundCols) "privacy" claim — bound non-writable
  columns ARE transmitted (e.g. email); only unmapped columns are
  excluded.

MEDIUM
- Fix typed-nil trap: any(op.Message) == nil is false for a nil *T;
  switch to op.Message.ProtoReflect().IsValid().
- Sort op.Where keys before iterating to keep SQL deterministic
  (otherwise pgx prepared-statement cache misses on every call).
- Validate op.Where keys exist in m.bindingsByColumn — prevents
  attacker-controlled identifier injection via the map-key surface.
- Tighten AutoSet conflict check: reject if column is *any* binding
  (writable or not), not just writable.
- Add pg_query_go-based validation for AutoSet SQL literal: parse and
  reject multi-statement / non-expression input.
- Remove faulty test-wording audit step — verified existing tests assert
  via connect.CodeOf(err), not on message text.
- Document soft-deleted user wire change: today's UpdateProfile returns
  Internal; aippatch returns NotFound. Add to Wire conformance note,
  rollout plan, and Risks.
- Fix cmd/server/main.go → cmd/drill/main.go (drill's actual binary
  path).

LOW
- UpdateAllWritable empty-mask path: explicit Internal in v0 (was
  silently emitting AutoSet-only UPDATE).
- Fix uuid.UUID(v).String() → uuid.UUID(v.([16]byte)).String() in type
  table.
- Switch validated bool to atomic.Bool for race safety under -race.
- Replace fictional maskHasPath with slices.Contains in handler example.
- Add field-number-vs-alphabetical ordering explanation to algorithm.
- Add GetPaths nil-safety note.
- Note PK appears in Bindings (no dedup with PK column).
- AutoSet test pattern: SELECT before / Apply / SELECT after — within a
  tx, NOW() is constant, so the framework's separate query advances it.
- Remove dead yaml override comments (free_full_educators_used skip
  entry was never visited).

NIT
- AIP-203 → AIP-134 §Update_Mask citation.
- "stale" → "superseded" sqlc deletion wording.
- Architecture diagram pg_query_go shows version.

Spec grew from ~770 to ~830 lines.
Both opus and sonnet round-3 reviews returned "Approve with minor revisions"
— no HIGH severity findings. Sonnet's "H1" was a redundant nil check that
opus correctly classified NIT (the IsValid check actually works); fixed as
cosmetic.

MEDIUM (both reviewers)
- Validate(codecs) now explicitly fails when a Binding references an
  enum codec name not in the supplied codecs map. Closes the silent
  failure mode where dangling refs would only surface at first Apply.
- Validate doc spells out FromText construction, dangling-codec check,
  and validated-stays-false-on-error.
- Pin huandu/go-sqlbuilder to @v1.36.0 in `go get` step (UpdateBuilder.
  Returning was added in that release).

LOW
- UpdateAllWritable empty-mask path now returns CodeUnimplemented (was
  Internal); AIP-aligned for "feature not implemented".
- SET-clause iteration now walks Mapping.Bindings order (alphabetical)
  filtered by mask membership, instead of client-supplied path order.
  Makes SQL deterministic regardless of how clients order the mask
  paths — pgx prepared-statement cache hits, and golden tests are
  stable.
- Add codegen step rejecting EnumCodec maps with duplicate ToText
  values (would produce a lossy FromText).
- Note `sqlbuilder.PostgreSQL.NewUpdateBuilder()` is required (default
  flavor would emit MySQL-style `?` placeholders).
- Fix AutoSet test pattern — NOW() is constant within a transaction so
  SELECT-before/Apply/SELECT-after inside BeginFunc would see equal
  values. Use clock_timestamp() OR top-level statements OR capture
  pre-Apply NOW() and assert post >= captured.

NIT
- Drop dead `op.Message.ProtoReflect() == nil` clause; ProtoReflect
  never returns nil for protoc-gen-go types.
- Hoist ProtoReflect() to a `src` local instead of calling repeatedly.
- Use `ub.IsNull(m.SoftDelete)` instead of string concatenation.
- Add bullet for ALTER TABLE ADD/DROP CONSTRAINT and other unlisted
  AlterTableCmd kinds being ignored in v0.
- Sort AutoSet alphabetically by Column for stable diff (in addition
  to Bindings).
- Move nested-mask-path rejection out of proto-field-iteration loop
  (proto field names never have dots; the check belongs at yaml
  validation time on writable/overrides keys).
- Document CGO first-build cost (~3 min) and CI cache requirement.
- Align spec on Makefile's existing `generate` target (was claiming a
  nonexistent `codegen` target).

Final spec is implementation-ready. Both reviewers gave "Approve with
minor revisions"; ending the review loop after round 3 per CLAUDE.md
("up to 3 rounds, end early if clean").
15 tasks across 6 phases covering migrations 015–018, the
internal/feat/idleunsub/ feature package (token sign/verify, trigger
evaluation, KeepSubscription, AutoReverse, email composition), Stripe
webhook integration, the public /sub/keep endpoint, the auth-middleware
AutoReverse hook, the user-info RPC + AckKeptBanner, and the React
KeptBanner component. Acceptance criteria + manual smoke test + spec
out-of-scope list at the end.

Spec: docs/superpowers/specs/2026-05-07-idle-auto-cancel-design.md
Round 4: opus + sonnet both rated "Approve with minor revisions". Both
flagged the same M1 (UpdateAllWritable error-code inconsistency across
four sections). Opus added M2 (codegen column-uniqueness) and several
LOWs/NITs; sonnet added decode fd-nil-guard.

MEDIUM
- Reconcile UpdateAllWritable across all four sections that mentioned it:
  Errors section now lists CodeUnimplemented; Roadmap aligns with Apply
  walkthrough's defense-in-depth check; yaml-mapping prose says codegen
  is the primary rejection point.
- Codegen step 10 added: verify column uniqueness across emitted
  bindings to catch overlapping override.column entries that would
  produce duplicate RETURNING and SET clauses Postgres rejects.
- Codegen step 9 added explicitly: yaml empty_mask: update_writable is
  rejected at codegen, not just at runtime.

LOW
- decode() in Apply step 7 now guards fd == nil before passing to decode,
  symmetric with encode() in step 2.
- AutoSet NOW() gotcha explained inline in the canonical yaml example
  (before, only in Testing strategy — readers who scrolled past would
  write a flaky test).
- Op.Where doc strings now state values must be scalar / pgx-bindable;
  list comparators (IN, !=, <) deferred to v1+.
- Generalize "call patches.InitPatches()" error message to "your
  generated InitPatches()" — the runtime is consumer-agnostic.
- PK-without-proto-field case explained: column simply doesn't appear
  in Bindings; WHERE uses m.PK for identity regardless.

NIT
- Decisions row 8 now matches the wire-conformance "permanent per
  resource" claim (was contradictorily saying "relax later").
- "drop" in algorithm step 3.iii reworded to "emit no binding; field
  is registered as intentionally skipped" so the relationship to step 4
  is unambiguous.
- Risks #1 first-build cost wording softened from "~3 minutes" to
  "several minutes (varies by runner)".
- Validate concurrency note added: callers serialize startup; sync.Once
  is left to the caller if needed.
Round 5: opus + sonnet both rated "Approve with minor revisions". Real
findings:

MEDIUM
- Validate concurrency wording was wrong: concurrent map writes ARE a
  Go data race even when content would be identical. Reword to "Validate
  is NOT safe for concurrent invocation on the same Mapping" and require
  callers to serialize. The generated InitPatches() does this naturally;
  sync.Once is left to callers needing strict guarantees.
- Document partial-clone discard on decode error: every error path in
  Apply returns zero T (not the partial result). Implementer must not
  return the in-progress clone on the error branch.
- AutoSet column identifier safety: codegen now restricts a.Column to
  match [A-Za-z_][A-Za-z0-9_]*. Reserved-word and special-character
  column names are rejected at codegen rather than handled at runtime
  with pq.QuoteIdentifier (deferred to v1 if needed).

LOW
- Add Diagnostics example for column-uniqueness check (algorithm step
  10) so it matches the pattern of every other step's diagnostic.
- Add Diagnostics example for AutoSet identifier-safety rejection.
- Reword Op.Where slice-value comment: it does not produce
  "col = ARRAY[…]" in the SQL string (the SQL is just "col = $1"); the
  array shape is a pgx wire-protocol artifact. Point preserved
  (slices won't IN-expand) without misleading SQL claim.
- Document Op.Where SQL-only-column constraint: keys must reference
  a bound column. Soft-delete timestamps, audit columns, tenant
  filters not in the proto cannot appear in Where. Framework's
  SoftDelete handling covers the most common case.
- Tighten CodeUnimplemented bullet to flag it as defense-in-depth
  only (consistent with Roadmap and Algorithm step 9 wording).

NIT
- Single bindingsByProto lookup in Apply step 2 (was double).
- Document encode/decode fd-nil checks as defense-in-depth — Validate
  proves these can't fire under normal codegen flow; the runtime
  guards are belt-and-suspenders against hand-edited generated files.
- Cross-link v1.36.0 floor in Decision #6 (was only in rollout step 1).
- Rollout step 9 says `make generate` (which runs sqlc generate +
  aippatchgen --check) instead of just "regenerate sqlc" — keeps
  aippatch in sync if a developer touches sql/queries.
Round 6: opus rated "Approve as-is" (first clean verdict). Sonnet found
one MEDIUM about the wire-change scope.

MEDIUM (Sonnet)
- Wire-change claim was narrower than reality. Verified:
  b.UpdateDisplayName (internal/backend/user.go:17-32) returns
  ErrUserNotFound for any pgx.ErrNoRows (soft-delete OR PK mismatch),
  and the handler wraps it as Internal. So today everything →
  Internal. With aippatch all zero-row cases (soft-delete, PK
  mismatch, future scope filters) → NotFound; pgx errors stay
  Internal. The wire change is broader than just soft-delete.
  Updated Wire conformance note, Risks #6, and rollout plan step 7
  to describe this accurately.

LOW (Opus)
- Note that op.Where allows non-writable bound columns (e.g. email
  for scoping a per-email UPDATE) — writability gates SET-clause
  emission, not WHERE-clause membership.
- Add note that multiple-invalid-mask-paths returns the first error
  in client-supplied order; tests should assert on connect.CodeOf,
  consistent with drill's existing handler tests.

LOW (Sonnet)
- proto.CloneOf version footnote: dropped specific minor version
  ("v1.36.6") — exact added-in version is uncertain; spec now just
  says "available in the protobuf-go versions used here" with the
  go.mod version cited.

NIT
- Annotate slices.Contains in handler example with Go-version note
  (stdlib since 1.21; drill on 1.25).
Sonnet review (3 HIGH, 4 MEDIUM, 5 LOW, 4 NIT). Opus timed out; will
re-run alongside sonnet for round 2 on the fixed plan.

HIGH
- Replace fake emptyMessage stub (didn't satisfy proto.Message) with
  *emptypb.Empty in Task 4's pre-fixture tests. Generic constraint now
  works without inventing a stub.
- Add ALTER TABLE … RENAME COLUMN handling to sql.go (was missing —
  spec Algorithm step 2 explicitly requires it). pg_query_go represents
  this as Node_RenameStmt at the top level; added applyRename + a
  testdata/sql_rename/ fixture and TestLoadSchema_RenameColumn.
- Drop Task 44's soft-delete wire test. Verified that
  GetAuthSessionByToken (sql/queries/auth_sessions.sql:6-16) joins on
  users.deleted_at IS NULL — a soft-deleted user can't authenticate at
  all, so the wire returns Unauthenticated, not NotFound. The runtime
  TestApply_SoftDeletedRowReturnsNotFound (Task 22) already covers the
  zero-row branch. Task 44 now just runs the existing handler tests.

MEDIUM
- Add empty-models guard + single-package check in run() — multi-package
  yaml is a v1+ extension; v0 returns a clear error.
- Strengthen Algorithm step 4 (every proto field accounted for) to emit
  an explicit diagnostic for unmatched fields, with deduplication
  against earlier diagnostics. Added TestProcessResource_UnmatchedProtoField_Diagnostic.
- Document mapping mutation safety in TestApply_UpdateAllWritable
  (mapping is unshared per-test; t.Parallel is safe).
- Move sort BEFORE the comparison loop in checkOutputs so iteration
  order is actually stable (was dead code after the loop returned).

LOW
- Drop dead `var _ = errors.New` from errors.go; document that the
  helpers accept %w-style wrapping.
- Add buf.yaml in fixturepb/ so buf can resolve google.protobuf.Timestamp;
  add `buf dep update` to the generation step.
- Add `os` import to TestRun_FixtureRoundTrip (was missing).
- Add yaml `go_package_path` field, eliminating the drill-only heuristic.
  Spanda projects MUST set it; e2e fixture and aippatch.yaml now do too.
- Update aippatch.yaml example to include go_package_path explicitly.

NIT
- Fix "Task 46" reference in Task 2's Makefile comment (actual wiring is
  Task 38).
- Update TestLoadYaml_Basic to assert GoPackagePath.
Both opus and sonnet "Revise" verdicts. Real correctness bugs in
round-1 patches.

HIGH (Opus)
- Delete dead `case pg_query.AlterTableType_AT_RenameColumn:` block
  from applyAlterTable. That constant doesn't exist in pg_query_go/v6;
  AlterTableCmd has no GetNewname method. Top-level Node_RenameStmt
  handling in applyRename is the only correct path. Verified against
  pganalyze/pg_query_go/v6 and libpg_query/17-latest.

HIGH (Sonnet)
- Fix enum constant naming in init.gen.go template. Previous template
  emitted `int32(drillv1.USER_ROLE_CANDIDATE)` but protoc-gen-go names
  the constant `drillv1.UserRole_USER_ROLE_CANDIDATE`. Renamed
  EnumValue.Name -> EnumValue.GoConst, populated in processResource
  with `<EnumName>_<VALUE_NAME>` form. Template now emits valid Go.

MEDIUM (both)
- Replace fragile substring dedup in step-4 unmatched-field loop with
  an explicit `diagnosedFields map[string]bool` populated at every
  diagnostic emission site. Substring scan would false-positive on
  any field name that's a substring of another (e.g. "name" ⊂
  "display_name").

LOW (Sonnet)
- Derive Go package alias from the proto file's go_package option's
  `;<alias>` suffix, not from the proto package name. Old code mapped
  `aippatch.fixture.v1` -> `aippatchfixturev1` which disagrees with
  fixturepb's actual `package fixturepb` declaration. New goPackageAlias
  helper reads FileDescriptor.Options() and parses the option's
  `<importPath>;<alias>` form.

LOW (both)
- Drop `var _ = pgx.ErrNoRows` import-keepalive in apply.go and remove
  the unused `pgx` import (DBTX uses pgx.Rows but only in types.go).

LOW (Opus)
- Rename `new` -> `newName` / `oldName` in applyRename to avoid
  shadowing the Go builtin.

NIT
- Simplify filename construction (single Split instead of double
  SplitAfter).
- Align unsupported-kinds diagnostic wording with spec's Diagnostics
  section ("unsupported in v0; mark skip:true").
Round 3: opus "Approve as-is", sonnet "Revise" but with only LOW + NIT
findings (technically should have been "Approve with minor revisions"
per format spec). Both verdicts confirm round-2 fixes landed cleanly.

Polish applied (all sonnet's findings):

LOW
- Task 28 buf build path was wrong (resolved relatively from a deep
  testdata directory). Reframe to run from repo root with the absolute
  output path. Eliminates a silent "descriptor not found" failure
  mode.
- Add code comment to run() explaining the single-import constraint
  for emitInit (already guarded; just document the why for future
  multi-package work).

NIT
- snakeCase already lowercases, drop redundant strings.ToLower in
  filename construction.
- Replace bespoke isUp helper with strings.HasSuffix in
  aippatchtest/db.go.
- Drop the (non-existent) double-wrap concern noted by sonnet —
  encode returns plain fmt.Errorf, not a connect.Error, so wrapping
  with connectInvalidArg is correct.

Skipped sonnet's emitInit-test-vs-Codecs note — informational, no
runtime impact in v0 (single-package guard handles it).

Plan is converged after 3 review rounds. Implementation-ready.
btc added 29 commits May 9, 2026 17:28
… stubbed)

TestProcessResource_Happy is intentionally failing: compatibility() returns
false for all field kinds until Task 32 fills it in.
… dup codec, update_writable, unmatched field)
…rPatch

Drop the manual protoreflect field-validation loop and hand-rolled
UpdateDisplayName call; delegate field validation and the UPDATE to
aippatch.Apply. Add patches.InitPatches() to the test TestMain so the
Mapping is validated before the handler tests run.
Cover auto_set_conflict (column also a binding), auto_set_bad_literal
(multi-statement expression), auto_set_column_not_found, and
auto_set_column_nullable. The missing_column case was already covered
by TestProcessResource_UnmatchedProtoField_Diagnostic.
Verify that Apply preserves unmapped proto fields from the input
message via proto.CloneOf — ensuring fields not in the Mapping's
Bindings pass through unchanged in the returned proto.
The map was built during auto_set validation but never read — the
comment "populated for validation; not stored in model" made this
look intentional, but the validation checks use columnOwners instead.
Remove the variable and its writes entirely.
Adds connectUnimplemented alongside connectInternal/connectInvalidArg/
connectNotFound so all connect error constructors follow the same
fmt.Errorf pattern. Updates Apply to use the new helper and drops the
now-unused errors import.
processAll, processResource, and emitResource took large structs by
value; gocritic hugeParam/rangeValCopy flags these. Switch all three
to pointer receivers and update callers to use index-based range loops
and address-of at call sites.
The previous parse-only check accepted multi-statement payloads
(e.g. "1); DROP TABLE x; SELECT (1") and comma-expanded SET payloads
(e.g. "NOW(), col = 'x'"). Inspect the parse tree: reject if stmt
count != 1, target list length != 1, or the single target is a RowExpr.

Add tests for both injection vectors.
Distinguish client-input errors from server-config errors in encode.
Define errEnumValueOutOfMap sentinel; wrap enum-out-of-map with it in
encode(); check errors.Is in Apply's SET-clause loop and map to
InvalidArgument rather than Internal.

Add TestApply_EnumOutOfMapReturnsInvalidArg to exercise the path.
After resolving the table, check that r.PK and r.SoftDelete (when
non-empty) name actual columns. Emit diagnostics with a hint listing
candidate column names. Tests cover missing pk and missing soft_delete.
Two codegen bugs fixed:
1. Discarded error from FindDescriptorByName — misspelled proto_enum
   now emits a diagnostic instead of shipping an empty codec.
2. yaml Map keys not matching any proto enum value name were silently
   dropped — now emit a diagnostic listing the unknown keys and valid
   names.

Tests cover both paths.
Verify Apply respects the caller's transaction: rolled-back tx leaves
the row unchanged; committed tx persists the update.
Add Apply function doc comment per spec (op.Message non-nil, returns
CloneOf). Add inline field doc comments to Mapping, Binding,
AutoSetClause, EnumCodec, and Op structs noting the ProtoEnum field
is metadata-only.
…ation

Replace raw "{{.X}}" interpolation in both templates with {{q .X}} where
q = strconv.Quote. This prevents generated code from becoming invalid
if a field value contains a double-quote or backslash. Defensive for
current constrained values; guards against future yaml typos.
Switch connectInternal("...: %s", err.Error()) to %w so underlying
errors participate in errors.Is/As chains.
resolveImportPath takes yamlResource by pointer; range loops over
models use index-based iteration to avoid rangeValCopy.
- .gitignore: ignore /aippatchgen binary (mirrors /drill, /drillctl).
- algorithm.go: pre-validate all declared codecs (proto_enum descriptor
  existence, yaml-key vs proto-value-name matching) up-front instead of
  gating on binding usage. Catches silent drift in unused codec yaml.
- main.go: replace `for i := range models[1:] { m := &models[1:][i] }`
  with `for i := 1; i < len(models); i++ { m := &models[i] }` — same
  backing array, easier to read.
- algorithm_test.go: fix misleading comment on
  TestProcessResource_AutoSetMultiTargetLiteral_Diagnostic — the parser
  produces a single RowExpr target, not two targets.

Round-2 review feedback (opus LOW + NIT items).
Round-2 review (opus) flagged that bindings referencing a broken codec
produced two diagnostics for the same root cause — once from the
processAll pre-validation pass, again from processResource's per-binding
codec-tracking branch.

Thread a brokenCodecs map[string]bool from processAll → processResource.
The per-binding branch now skips emission entirely (continues) for
codecs already flagged broken. Pre-validation guarantees a clean enum
descriptor + unique ToText + matching yaml keys for codecs that aren't
in brokenCodecs, so the per-binding lookup is now a straight cast.

Tests: two assertions adjusted to match pre-validation wording (the
per-binding wording is no longer reachable for these failure modes).
@btc btc force-pushed the worktree-aippatch-impl branch from 1cfc2aa to 057b1b0 Compare May 9, 2026 21:29
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