Skip to content

feat(rpsql): add pkg/rpsql go-jet query layer for Redpanda SQL [AI-1675] - #2

Draft
c-julin wants to merge 4 commits into
mainfrom
jc/AI-1675-rpsql
Draft

feat(rpsql): add pkg/rpsql go-jet query layer for Redpanda SQL [AI-1675]#2
c-julin wants to merge 4 commits into
mainfrom
jc/AI-1675-rpsql

Conversation

@c-julin

@c-julin c-julin commented Jul 22, 2026

Copy link
Copy Markdown

Summary

Adds pkg/rpsql — a type-safe query layer for Redpanda SQL (rpsql) built on go-jet. Primary driver: reading external catalog (Iceberg/Kafka) tables via the non-standard catalog => table operator, which go-jet's typed table API cannot emit.

Jira: AI-1675.

What it adds

  • CatalogTable — reads an external catalog table by wrapping SELECT * FROM catalog => table in a CTE. Only that one-line seed is raw; the outer query (projections, WHERE, GROUP BY, aggregates, composite access) is fully type-safe.
  • Field / FieldString / FieldInt / … — composite (struct) member access (col).field, including nested composites. go-jet emits col.field, which rpsql parses as table.column, so this is required.
  • ExecArgs / Query — run a go-jet statement over a pgx Querier under QueryExecModeExec (required: rpsql describes untyped bind params as text, so pgx's default mode can't encode non-text params).
  • gen + rpsql-jet-gen CLI — generate go-jet table/model code by introspecting a live rpsql via information_schema (the stock go-jet generator relies on regclass, which rpsql lacks). Reuses postgres.Dialect templates so output shape matches the rest of the repo.

Why go-jet + a CTE

  • rpsql can't be a native go-jet dialect (the dialect constructor is internal), but it's Postgres-wire-compatible, so the stock postgres dialect builds correct read SQL.
  • External catalog tables are reachable only via =>, which go-jet cannot emit (identifier quoting + an unexported serialize interface). The CTE confines => to a raw seed while its plain-identifier name gives the outer query a typed, addressable table.

rpsql read-side constraints (handled / documented)

No ILIKE, DISTINCT ON, RETURNING, ON CONFLICT, or #> / #>> path operators. Arrow operators -> / ->> work on json/jsonb; composite access uses (col).field.

Testing

  • Unit (go test ./pkg/rpsql/): SQL-serialization assertions for Field and CatalogTable, no DB.
  • Integration (-tags integration, gated on a reachable rpsql + RPSQL_TEST_* env): managed-table aggregate, composite field access, generator round-trip, and an external catalog read against a neutral orders_seed_test fixture. All pass against a live localdev rpsql.

Notes

  • Draft: opening for early review of the API shape (CatalogTable / Field / generator split) before wiring into a consumer.
  • No go.mod changes — uses the repo's existing go-jet / pgx / lib-pq deps.

c-julin added 4 commits July 22, 2026 15:25
Adds pkg/rpsql: a type-safe query layer for Redpanda SQL (rpsql) built on
go-jet, focused on reading external catalog (Iceberg/Kafka) tables via the
catalog => table operator.

- CatalogTable: wraps a catalog => table read in a CTE so only the raw seed
  is untyped; the outer query (projections, WHERE, GROUP BY, aggregates,
  composite access) stays fully type-safe.
- Field/FieldString/FieldInt/...: composite (col).field member access.
- ExecArgs/Query: run go-jet statements over a pgx Querier under
  QueryExecModeExec (required by rpsql's text-defaulted bind params).
- gen + rpsql-jet-gen CLI: generate go-jet table/model code by introspecting
  a live rpsql via information_schema (go-jet's own generator relies on
  regclass, which rpsql lacks).

Reuses the repo's existing go-jet/pgx deps; no go.mod changes. Unit +
build-tagged integration tests validated against a live rpsql instance.
…ad) [AI-1675]

Adds a proto-first Redpanda SQL read-model generator to the plugin: annotate
a message with (gojet.v1.rpsql_read){catalog, table, cte_name} and the plugin
emits a type-safe go-jet read model bound to catalog => table (pkg/rpsql.
CatalogTable) in the normal buf generate pass — no DDL, migrations, or Docker.

- options.proto: RpsqlRead message + MessageOptions extension (field 77232).
- resolve_rpsql.go: maps scalar/enum/timestamp/duration -> typed columns and
  recurses single nested messages into composite (col).field accessors
  (incl. nested composites). Repeated/map fields are skipped: rpsql cannot yet
  read array / composite-array columns (variant planner error, no UNNEST).
- emit_rpsql.go: hand-written emitter -> <Message>Read with CTE() + accessors.
- main.go: resolve/emit read models alongside the write path, bypassing FK
  resolution and the DDL/Docker pass.
- e2e: RpsqlOrder fixture (scalars, enum, Timestamp, single + nested composite,
  skipped repeated/map) + generated read model; SQL-shape test and a live-rpsql
  integration test (validated against a neutral orders_seed_test fixture).
The read model now assembles proto messages directly instead of requiring a
hand-defined scan struct: <Message>Read.Select(ctx, db, where...) returns
[]*<pb.Message>. It projects leaf columns (composite members via Field), scans
into an unexported row, and builds the proto — enum text->value, timestamptz->
timestamppb, nested composite->nested message, honoring proto presence. Repeated
fields stay zero-valued (rpsql array limitation). Validated against live rpsql
(orders_seed_test) via the e2e integration test.
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