Entlite
Entity-first generator for SQLC and Proto files. Maps DB and Protobuf types automatically to maintain a single source of truth in Go services.
Documentation: docs/ — getting started, reference, examples.
TODO
-
Wire buf lint into the pipeline, it needs the proto package and rpc response type decisions first
-
Implement DefaultFunc for sql generation
-
Explore google AIP https://google.aip.dev/ - read it through first and decide which parts fit, query and rpc naming is the open one
-
Add mcp for visual testing of examples/
-
Fix Optional() with Validate() - generated code passes a pointer to a value func and does not compile
-
Fix Time().Optional() on postgres/mysql - sqlToGo/goFromSQL have no time case, sqlc gives sql.NullTime but the wrapper declares *time.Time, so the converter does not compile. Needs a NullTimeToPtr helper
-
Add postgres and mysql optional examples
-
Fix filter.Optional() - it only makes the proto field optional, the sql keeps a plain comparison, so an omitted value filters on the zero value instead of being skipped. Needs sqlc.narg() with an IS NULL guard. The doc comment and the 03-optional readme both claim it is skipped
-
Implement Queries Having()
-
Aggregate a row count per group, Count() already means the total next to a page
-
Min()/Max() of a time column, sqlite cannot cast a timestamp back, postgres and mysql can
-
DeleteBy(...) query
-
Create queries example with complicated edge case queries
-
Figure out enums
-
Create /internal/naming/ to have in one place consistant naming
-
Figure out migration
Folder structure
ent/
├── schema/ # you write: one file per entity
├── logic/ # you write: Go funcs for DefaultFunc and Validate
├── contract/ # the input for sqlc and buf
│ ├── proto/ # generated: schema.proto, plus your custom .proto
│ └── sqlc/ # generated: schema.sql, queries.sql, plus your custom .sql
├── gen/ # the output, rewritten on every run
│ ├── db/ # generated: typed wrapper, pointers instead of null types
│ │ └── internal/ # generated: raw sqlc output, never import it
│ ├── pb/ # generated: proto messages and the connect service
│ └── ts/ # generated: TypeScript client
├── buf.yaml
├── buf.gen.yaml
├── buf.lock
├── sqlc.yaml # dialect and sqlc settings
└── generate.go # the go:generate lines that run the pipeline
Get started
sql dialect flag: postgresql (default) or sqlite or mysql
arguments: entity names
go run github.com/guntisdev/entlite/cmd/entlite new --dialect sqlite User Post
Examples
| Example |
Teaches |
| 01-basic-entity |
Every field type: String, Int, Int64, Float, Bool, Byte, Time, JSON Field options: Unique(), Optional(), Immutable(), Default(), DefaultFunc()
Validate() with your own Go function from the logic package Field level Contracts(): a password that clients write but never read, timestamps they read but never write Ready made queries: DefaultCRUD(), CreateBulk(), ListAll(), DeleteAll()
CreateBulk().Upsert("email"): re-importing a row overwrites it instead of failing on the unique email Queries by field: GetBy("email"), ListBy("is_active"), and Name() to rename one Filters: filter.Range() and filter.Search() Indexes: multi column index.Asc() / index.Desc(), mixed sort order, Unique(), Name() The same schema on three dialects: sqlite, postgresql, mysql |
| 02-custom |
Hand-written custom.sql and custom.proto live beside the generated files and survive regeneration A hand-written service that reuses a generated proto message Virtual fields: Contracts(entlite.PROTO()) gives a field with no column, filled in by the server Choosing the key type per entity: field.Int64("id") on a high volume table A foreign key follows the type of the entity it points at Query level Contracts(): a query that stays in the database layer and gets no rpc Two entities in one schema A multi-word entity name: SensorReading becomes the table sensor_reading, which sqlc reads back as the Go type SensorReading |
| 03-optional |
Optional() on every type that supports it, one field each
Bool cannot be optional — use Default(false) instead An optional field becomes optional in proto and a nullable column in SQL
filter.Eq("is_featured").Optional() — a required field used as an optional filter A string uuid primary key: Immutable(), DefaultFunc() and PROTO().ReadOnly() Server generated fields drop out of create and update requests
Create().Upsert("slug").Ignore() — a taken slug keeps the stored row instead of failing |
| 04-contracts |
SQLC() only — a table with no proto message and no service
PROTO() only — a message and a service with no table Both — a table served to clients
PROTO().ReadOnly() — a table and a service, but no write rpc Query level Contracts(): a query the server runs that gets no rpc Field level PROTO().ReadOnly() for server managed timestamps Contracts on an entity are the default; a field or query can narrow them
index.Primary("name") — a natural key, the entity gets no generated id column |
| 05-queries |
Ready made queries: Create(), CreateBulk(), Get(), Update(), Delete(), DeleteAll(), ListAll()
GetBy("commit_sha") and ListBy("branch") — the fields you name become = filters Filters spell out the operator: Eq is =, Range is a lower and upper bound, Search is LIKE
Asc() and Desc() chained, the chain order is the order of the ORDER BY columns
Limit()/Offset() take their values from the caller, a fixed Limit(10) stays in the SQL
Count() returns the total before LIMIT, next to the page of rows
Distinct("branch") returns column values, not rows, so the method gives []string
Distinct("branch", "status") dedupes on the tuple and returns the query's own row struct
GroupBy("branch") with Sum() returns one row per group, in the query's own row struct
Sum(), Avg(), Min(), Max() without a GroupBy fold the whole table into one row
Contracts(entlite.SQLC()) on a query keeps it out of the API, only the database method is generated A comment directly above a query becomes the comment of the generated SQL and rpc |
Launch example
Each example has a Makefile that generates types, bundles the JavaScript and starts the web server
cd examples/01-basic-entity/sqlite
make run
The postgresql and mysql variants run their database from docker-compose.yml, make run starts it
and waits until it is ready. Use make down to stop it and drop its data
cd examples/01-basic-entity/postgresql
make run
Individual steps are available as make gen, make web and (for postgresql/mysql) make db.
make clean removes node_modules, the bundle and the database
Doing it by hand, generate types
cd examples/01-basic-entity/sqlite
cd ent/
go generate
Build JavaScript
cd web/
npm install
npm run build
Run go web server
go run main.go
Release
# Tag and push version
git tag v0.0.3
git push origin v0.0.3
# Force Go proxy cache update
GOPROXY=https://proxy.golang.org GOPRIVATE= go list -m github.com/guntisdev/entlite@v0.0.3
# Update CLI tool & library dependencies in projects
go install github.com/guntisdev/entlite/cmd/entlite@v0.0.3
go get github.com/guntisdev/entlite@v0.0.3