protoc/buf plugin that turns .proto messages annotated with sqlmap extensions (table/column metadata, primary keys, foreign keys) into a <file>.sqlmap.go per proto file, containing:
schema—schema.v1.SchemaTable/SchemaColumnvalues describing every table, consumed bypkg/migrationto create tables (via ariga/atlas) or generate migration files.scanner— a<Message>Resultstruct per table withScan,GetColValue, and column-list helpers, for reading rows into Go without reflection.query— FieldMask-awareLoad<Message>/Load<Message>Rowsfunctions that select only requested columns and eagerly load related messages (one batchedIN (...)per relation, stitched in Go), consumed bypkg/query.
These are independently switchable via the plugins= parameter (see below); schema, scanner, and query are the three plugins that exist today.
go install github.com/snaerverk/protoc-gen-go-sqlmap/cmd/protoc-gen-go-sqlmap@latestprotoc --go-sqlmap_out=paths=source_relative:. your.protoOptional plugins= parameter (comma-separated, protoc-style) selects which of schema, scanner, query to emit, +-separated:
protoc --go-sqlmap_out=plugins=schema+scanner+query,paths=source_relative:. your.protoOmit plugins= to enable all three. query requires scanner to also be enabled, since its output calls the scanner's Result type. The schema writer emits a SchemaType entry for every dialect (mysql/postgres/sqlite3) regardless — the actual dialect is chosen at runtime by migration.New, not at generation time.
Add the plugin to buf.gen.yaml, either from BSR (no local install needed):
plugins:
- remote: buf.build/snaerverk/go-sqlmap:v0.1.0
out: pkg/generated/
opt: [paths=source_relative, plugins=schema+scanner+query]or as a local binary after go install:
plugins:
- local: protoc-gen-go-sqlmap
out: pkg/generated/
opt: [paths=source_relative]Import sqlmap/v1/sqlmap.proto and annotate tables and columns:
syntax = "proto3";
package example;
import "sqlmap/v1/sqlmap.proto";
option go_package = "example/examplepb";
message Author {
option (sqlmap.v1.table) = { name: "tbl_author" };
int64 id = 1 [(sqlmap.v1.col) = { fieldname: "author_id", pk: PK_AUTO }];
string name = 2 [(sqlmap.v1.col) = { fieldname: "author_name" }];
// has-many: the key lives on tbl_book (author_id), not here.
repeated Book books = 3 [(sqlmap.v1.col) = {
foreign_key: { entity: "Book", fieldnames: ["author_id"] }
}];
}
message Book {
option (sqlmap.v1.table) = {
name: "tbl_book"
foreign_keys: [{
fieldnames: ["author_id"]
to: { entity: "Author", fieldnames: ["author_id"] }
on_delete: ON_DELETE_CASCADE
}]
};
int64 id = 1 [(sqlmap.v1.col) = { fieldname: "book_id", pk: PK_AUTO }];
string title = 2 [(sqlmap.v1.col) = { fieldname: "book_title" }];
// proto3 optional gives this field presence, so it's nullable; a bare
// scalar has no presence and would generate NOT NULL instead.
optional int64 author_id = 3 [(sqlmap.v1.col) = { fieldname: "author_id" }];
}pk: PK_AUTOfor a database-generated key,PK_MANfor one the application supplies.- A
message-kind field withforeign_keyis a relation:repeatedis has-many (relation only, no column); a singular field is belongs-to (both a column holding the key and a relation). - Nullability: an explicit
nullableon the column wins; otherwise PKs areNOT NULLand every other column follows the proto field's presence — aproto3 optionalor message-kind field has presence and is nullable, a bare proto3 scalar does not and becomesNOT NULL. - A
message-kind field without aforeign_keyis stored as an embeddedJSONcolumn.
See proto/sqlmap/v1/sqlmap.proto for the full extension surface (ForeignKeyDefinition, per-dialect type overrides) and docs/design/DESIGN-SUBTYPE-TABLES.md for joined-table subtype hierarchies (oneof + (sqlmap.v1.subtypes)).
db, _ := sql.Open("postgres", dsn)
// Create tables from the generated schema (or use m.Diff/ApplyPending for
// migration files instead).
m, err := migration.New(db, migration.DialectPostgres)
if err != nil {
return err
}
if err := m.Create(ctx, examplepb.AuthorTable, examplepb.BookTable); err != nil {
return err
}
// Load authors with their books' titles eagerly loaded, selecting only the
// columns the FieldMask asks for.
conn := query.Conn{DB: db, Dialect: query.Postgres}
authors, err := examplepb.LoadAuthor(ctx, conn,
&fieldmaskpb.FieldMask{Paths: []string{"name", "books.title"}})LoadAuthor also takes ...query.Cond filters (build one with query.In(col, values), which reports whether the value slice was non-empty). For row-level access without eager loading, use <Message>Result.Scan directly against GetAuthorColumns()/GetAuthorPKColumns().
task install # go mod tidy && go install ./cmd/protoc-gen-go-sqlmap/
make install # buf generate && go install (regenerates pkg/generated/** from proto/)
buf generate # regenerate pkg/generated/**.pb.go from proto/
go build ./...
go test ./... # golden + unit tests
go test ./pkg/generator/sqlmap/ -update # accept new golden output
SQLMAP_E2E=1 go test ./pkg/generator/sqlmap/ -run TestE2E # dockerized-Postgres end-to-end- pkg.go.dev
docs/design/DESIGN-SUBTYPE-TABLES.md— joined-table subtype hierarchies.docs/design/DESIGN-FILTRIFY-CONDITIONS.md— planned: translating a filtrify AST into SQL filters at runtime.
- Schema generation + atlas-backed migrations (create tables, diff, migration files)
- FieldMask-aware queries with eager loading (has-many and belongs-to, arbitrary depth)
- Joined-table subtypes (
oneofenforced as a DB-level FK/CHECK constraint) - Multiple dialects: PostgreSQL, MySQL/MariaDB, SQLite
- Published as a BSR remote plugin (
buf.build/snaerverk/go-sqlmap) - SQL filters translated at runtime from a filtrify AST, replacing ad-hoc
query.Condconstruction - Insert/update helpers (currently out of scope;
pkg/migrationonly creates schema,pkg/queryonly reads)