feat(builder): InsertDefaults for INSERT ... DEFAULT VALUES#52
feat(builder): InsertDefaults for INSERT ... DEFAULT VALUES#52klaidliadon wants to merge 2 commits into
Conversation
Closes #51. Follow-up to #50. After #50, InsertRecord on an all-zero record errored out and pointed callers at sq.Expr as the escape hatch — bypassing the type-safe builder. The legitimate use cases (sequence tables, audit rows, any all-DB-defaulted insert) deserve a first-class path. DefaultValuesBuilder is a separate small type rather than overrides on pgkit.InsertBuilder. Earlier drafts tried to embed sq.InsertBuilder and shadow ToSql + Suffix, but that pretends to support 22 squirrel chain methods while only handling 2 — every other inherited method silently strips pgkit state. The new type exposes exactly what it supports: ToSql, Suffix(string), Err. DB.SQL.InsertDefaults("orders").Suffix(`RETURNING "id"`) // INSERT INTO orders DEFAULT VALUES RETURNING "id" Empty table is a build-time error via Err (raw, no pgkit: prefix — Querier.wrapErr adds it at use time). Suffix takes literal SQL only; placeholder-bearing suffixes (ON CONFLICT ... SET col = ?) fall back to sq.Expr at the call site. InsertRecord's empty-cols error now hints at SQL.InsertDefaults rather than sq.Expr; tableName empty case prints a generic placeholder. Tests: - 6 unit tests covering plain SQL, RETURNING, chained Suffix, EXCLUDED upsert, empty-table rejection, and the InsertRecord hint update - new default_only PG table in test schema + integration round-trip test exec'ing DEFAULT VALUES RETURNING id Planned via three Codex review iterations (auto-detect rejected, embed-and-shadow rejected, separate-type green-lit on v4). Plan at tmp/insert-default-values/2026-06-02-plan.md. Pre-existing pgkit.InsertBuilder state-loss bugs surfaced during review (Suffix drops err; table.go:93 chain returns sq.InsertBuilder mid-chain) are out of scope here and will land as a follow-up issue.
Code ReviewOne confirmed correctness bug, three low-severity design notes. 🔴 #1 — Unquoted table identifier (
|
Three of David's four findings on #52: - Value receiver on InsertDefaults to match the majority of sibling methods (UpdateRecord, UpdateRecordColumns, InsertRecords). InsertRecord stays *StatementBuilder for now; widening the inconsistency isn't justified. - Defensive table check in DefaultValuesBuilder.ToSql so a direct zero-value construction (var b pgkit.DefaultValuesBuilder) errors cleanly instead of emitting invalid SQL at exec time. - Suffix("") no-op so conditional-suffix callers don't get a trailing space that breaks SQL-string snapshot tests. Push back on the fourth finding (table identifier quoting) — the bug exists pgkit-wide (squirrel doesn't quote either, see Insert / Into across the repo). Fixing only InsertDefaults would create asymmetric API behavior. Will track as a separate pgkit-wide issue.
|
Pushed #1 — Table identifier quoting: Pushing back. The premise that "every other builder delegates to squirrel's #2 — Pointer receiver: Accepted. #3 — Zero-value bypass: Accepted. #4 — |
After #50,
InsertRecordrejects records whoseMapreturns zero columns and points callers atsq.Expras the escape hatch — bypassing the type-safe builder. Legitimate use cases (sequence tables, audit rows, any all-DB-defaulted insert) deserve a first-class path.Closes #51.
Design
DefaultValuesBuilderis a separate small type, not overrides onpgkit.InsertBuilder. The plan went through three Codex review rounds before settling here:pgkit.InsertBuilderembedssq.InsertBuilder(22 chain methods). OverridingToSql+Suffixonly handles 2; the other 20 inherited methods silently strip pgkit state. Dishonest surface area.ToSql,Suffix(string),Err. Codex green-light on v4.Full plan + decision log:
tmp/insert-default-values/2026-06-02-plan.md(ignored, branch-only).Behaviour
InsertDefaults("t").ToSql()INSERT INTO t DEFAULT VALUESInsertDefaults("t").Suffix(\RETURNING "id"`)`INSERT INTO t DEFAULT VALUES RETURNING "id".Suffix(...)InsertDefaults("")Err()InsertRecord(allDefault)(existing #50 path)SQL.InsertDefaults("t")instead ofsq.ExprSuffixtakes literal SQL only. Placeholder-bearing suffixes (ON CONFLICT ... SET col = ?) fall back tosq.Exprat the call site — placeholder rebinding withoutsq.StatementBuilderType'sPlaceholderFormatgetter is non-trivial and defer-able until concrete demand surfaces.Error from
InsertDefaults("")is raw (nopgkit:prefix).Querier.wrapErratquerier.go:23,47,71adds the prefix at use time — double-wrapping would surfacepgkit: pgkit: ....Test plan
make db-reset test-allagainst PostgreSQL 18.3 — all 6 packages green.RETURNING, chained Suffix, EXCLUDED upsert, empty-table rejection,InsertRecordhint update.TestInsertDefaultsRoundTrip: execINSERT INTO default_only DEFAULT VALUES RETURNING idagainst the newdefault_onlytable (every column DB-defaulted, isolates the contract).go vet ./...clean.Out of scope (tracked separately)
Codex's review surfaced two latent state-loss bugs in
pgkit.InsertBuilder's embed-and-inherit pattern that pre-date this PR:pgkit.InsertBuilder.Suffixdropserr— inherited fromsq.InsertBuilder, returns the squirrel type, loses the pgkit wrapper.table.go:93chainInsertRecord(r).Into(t).Suffix("...")loses pgkit state via.Into(...).Both are symptoms of the same disease — embedding-and-inheriting chain methods that return the parent type. The fix is to wrap every chain method on pgkit's side. Out of scope here; will land as a follow-up issue titled "pgkit.InsertBuilder loses state through inherited squirrel methods."