Skip to content

codegen: adopt go-optional (Option[T]) across the generated surface, add :maybe-one - #21

Merged
moznion merged 2 commits into
mainfrom
go-optional-adoption
Aug 9, 2026
Merged

codegen: adopt go-optional (Option[T]) across the generated surface, add :maybe-one#21
moznion merged 2 commits into
mainfrom
go-optional-adoption

Conversation

@moznion

@moznion moznion commented Aug 9, 2026

Copy link
Copy Markdown
Owner

What

Generated code now represents absence with moznion/go-optional's Option[T] instead of bare pointers — uniformly, with no pointer fallback and no config knob:

  • @if-present parametersoptional.Option[T] fields. None (the zero value) omits the guarded fragments; presence checks compile to arg.X.IsSome().
  • Nullable result columns (P5) → optional.Option[T] row fields.
  • New :maybe-one annotation:one with "no row" as a normal outcome: returns (optional.Option[Row], error) and maps pgx.ErrNoRows / sql.ErrNoRows to (None, nil).
  • The generated Ptr[T] helper is removed; call sites write optional.Some(v).

The driver boundary is unchanged, by construction

  • Optional parameters bind via UnwrapAsPtr() — the driver sees exactly the *T it saw before; runtime.BuildArgs/ResolveArgs untouched.
  • Nullable columns scan into per-column *T temporaries and convert with optional.FromNillable afterwards.

Nothing rides on go-optional's sql.Scanner/driver.Valuer: pgx routes unknown Scanners through a lossy driver.Value detour (numeric et al.), and Option[T]'s underlying []T shape is ambiguous to pgx's reflection-based plan selection. docs/design/17-go-optional-adoption.md records the decision; a future pgx-native codec (go-optional/pgxoption, separate PR) can enable direct scanning as an optimization.

Not touched, deliberately

  • Renderings, cache JSON, shape keys, Compose conformance — SQL bytes and bind order are byte-identical to before.
  • internal/config's tri-state *bool (yaml.v3 has no decode-side TextUnmarshaler), internal comma-ok APIs, runtime/'s public types, and the stdlib-only LSP.

Verification

  • Unit suites green, including new pins: Option struct fields, IsSome/UnwrapAsPtr emission, *T-temporary scan path, :maybe-one for both placeholder styles (pgx and database/sql flavors).
  • go test -tags devdb ./internal/e2e/ fully green (postgres + mysql + sqlite containers): NULL-heavy scans into Option fields, Some/None bind shapes, maybe-one hit (Some with None column) and miss (None, no error) on all three dialects; cold→warm CLI round-trips; native-oracle corpus byte-identity gates.
  • golangci-lint run --build-tags devdb ./... and standalone staticcheck -checks all: 0 issues; goimports clean (generated import blocks are goimports-stable).
  • Examples regenerated for all three dialects with a FindUserByEmail :maybe-one showcase; spec, manual (getting-started / template-language / runtime / sqlc-migration), README, and CLAUDE.md updated.

🤖 Generated with Claude Code

https://claude.ai/code/session_01P7UbMMpmzxDECU9gqxAAZ3

Generated code now represents absence with moznion/go-optional's
Option[T] instead of bare pointers, uniformly and with no opt-out:

- @if-present parameters become optional.Option[T] fields (None omits
  the guarded fragments; presence checks compile to IsSome()).
- Nullable result columns become optional.Option[T] row fields.
- New :maybe-one annotation: :one with "no row" as a normal outcome —
  returns (optional.Option[Row], error) and maps pgx.ErrNoRows /
  sql.ErrNoRows to (None, nil).

The driver boundary is deliberately unchanged: optional parameters
bind via UnwrapAsPtr() (the same *T the driver saw before), and
nullable columns scan through *T temporaries converted with
optional.FromNillable — nothing rides on Option's sql.Scanner, whose
pgx path detours through a lossy driver.Value conversion.

The generated Ptr helper is gone; callers write optional.Some(v).
Examples regenerated for all three dialects with a FindUserByEmail
:maybe-one showcase; the devdb E2E suites cover Some/None binding,
NULL-heavy scans into Option fields, and the maybe-one hit/miss paths
on postgres, mysql, and sqlite. Design doc 17 records the decision
and the deliberate non-goals (config tri-state, internal comma-ok
APIs, LSP).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P7UbMMpmzxDECU9gqxAAZ3
The examples' main packages import it directly; go mod tidy moves it
out of the indirect block.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P7UbMMpmzxDECU9gqxAAZ3
@moznion
moznion merged commit 6272a91 into main Aug 9, 2026
5 checks passed
moznion added a commit that referenced this pull request Aug 9, 2026
Routine currency bumps flagged by the dependency audit:

- google.golang.org/protobuf v1.34.2 -> v1.36.11 (pg_query parse-tree
  decoding; ~2 years behind)
- testcontainers-go (+ mysql/postgres modules) v0.43.0 -> v0.44.0
- ncruces/go-sqlite3 v0.35.2 -> v0.35.3
- transitively: golang.org/x/sys v0.47.0, x/text v0.40.0

Rebuilt on top of the merged #21/#23 main. Verified with the full
devdb E2E suite (real postgres/mysql containers under the new
testcontainers, in-process sqlite on the new wasm build) plus the
unit suites; the corpus byte-identity gates pin that the protobuf
bump changes no oracle/catalog bytes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P7UbMMpmzxDECU9gqxAAZ3
@moznion
moznion deleted the go-optional-adoption branch August 20, 2026 00:40
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