Comparing sqlc, Ent, and Bob
At some point in every Go backend, you hit this decision: do you write SQL by hand, define schemas in Go, or let the database drive everything?
Most people land on sqlc or Ent. There's a third option most people miss: bob, a type-safe query builder that reads your existing database schema.
The biggest difference between these tools is where the source of truth lives: your SQL queries, a Go schema definition, or the database itself. If you already have a database schema, that decision is mostly made for you.
This post compares the three honestly. Each tool makes different tradeoffs, and the right choice depends on what you're building. We'll also cover how kiln - a schema-first API compiler for Go - sits on top of bob to generate a complete API layer - but the core comparison is between the database tools themselves.
What about GORM? GORM is the most popular Go ORM, but it's a fundamentally different approach. GORM uses runtime reflection to map structs to queries - you define your models, and GORM figures out the SQL at runtime. sqlc, Ent, and bob are all code generators - they produce typed Go code at build time, so the compiler catches mistakes before your code runs.
The comparison at a glance
| sqlc | Ent | bob | kiln (bob + API) | |
|---|---|---|---|---|
| Source of truth | Schema + SQL queries | Go schema | Database schema | Database schema |
| What it generates | Go types + query functions | ORM with graph traversal | Type-safe query builder + models (+ optional query-to-code) | Full API (handlers, routes, OpenAPI) |
| Runtime dependency | None | Ent runtime | bob (lightweight) | bob (lightweight) |
| You write | SQL queries | Go schema definitions | Nothing - it reads your database | kiln.yaml + business logic |
| Database migrations | You handle them | Built-in | You handle them | You handle them |
| Schema introspection | No | No (it owns the schema) | Yes | Yes (via bob) |
| Best for | Complex queries, full control | Graph-like data, complex relationships | Schema-first queries | Schema-first CRUD APIs |
sqlc
sqlc generates type-safe Go code from SQL queries you write. You provide your database schema and your queries, and sqlc uses the schema to assign the correct types to each query's parameters and results.
-- name: GetUser :one
SELECT * FROM users WHERE id = $1;
-- name: ListUsers :many
SELECT * FROM users ORDER BY created_at;
sqlc turns this into type-safe Go functions. No runtime dependency. You get exactly the queries you write, nothing more.
Where sqlc excels:
- Complex, hand-tuned queries (joins, CTEs, aggregations)
- Full control over every SQL statement
- Zero runtime overhead
- Teams that think in SQL
Where sqlc stops:
- You write every query by hand - each table needs its own set of CRUD queries
- No HTTP handlers, routing, validation, pagination, or OpenAPI
- Relationships don't generate anything automatically
- Adding a new table means writing all the queries, handlers, and routes manually
sqlc optimises for query control. You think in SQL files - write the query, get the Go function. If your app has complex reporting queries, aggregations across tables, or hand-tuned SQL that matters for performance, sqlc gives you exactly what you need with no surprises.
Ent
Ent is a Go ORM built by Meta. You define your schema in Go, and Ent generates a typed client with graph traversal, migrations, and hooks.
// ent/schema/user.go
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name"),
field.String("email").Unique(),
}
}
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("posts", Post.Type),
}
}
Ent generates a powerful graph-based query API:
users, _ := client.User.Query().
Where(user.NameContains("alice")).
WithPosts().
All(ctx)
Where Ent excels:
- Complex relationship traversal (friends-of-friends, recursive queries)
- Built-in migrations from the Go schema
- Privacy layer and hooks for cross-cutting concerns
- Teams that prefer defining schemas in Go
Where Ent stops:
- Your Go schema is the source of truth, not the database - if you have an existing database, you're duplicating it
- No HTTP handlers, routing, or OpenAPI generation
- Heavy runtime - the generated client has significant abstraction
- Learning curve for the graph API and schema DSL
Ent optimises for modelling. You think in Go schema definitions and graph edges - define the shape, Ent handles the rest. If your data has complex relationship patterns and you want the ORM to own the schema, Ent is comprehensive.
bob
bob takes a different approach from both. With sqlc, you write queries. With Ent, you define a schema. With bob, you point it at your database and it figures the rest out.
Point bob at a PostgreSQL, MySQL, or SQLite database, and it produces:
- Go models that match your tables exactly
- A type-safe query builder for each table
- Relationship-aware structs derived from your foreign keys
Bob also supports query-to-code generation - similar to sqlc, you can write SQL queries and bob generates type-safe Go functions from them. So you get both: schema-driven models for standard operations, and hand-written queries when you need them.
// Query users - type-safe, no raw SQL
users, err := models.Users.Query(
ctx, db,
sm.Where(models.UserColumns.Role.EQ("admin")),
sm.OrderBy(models.UserColumns.CreatedAt).Desc(),
)
Where bob excels:
- Your database is the source of truth - no schema duplication
- Type-safe queries without writing SQL or learning a DSL
- Multi-dialect support (PostgreSQL, MySQL, SQLite) from the same codebase
- Rich schema introspection - captures columns, types, constraints, relationships, and defaults
- Lightweight runtime with no implicit behaviour
Where bob stops:
- No HTTP handlers, routing, validation, or OpenAPI generation - you still build the API layer yourself
- Smaller community and thinner documentation than sqlc or Ent - expect to read source code occasionally, especially for advanced queries
- Younger library with an evolving API surface - minor breaking changes between versions are possible
bob optimises for schema-first development. You think in your database schema directly - bob reads it and gives you Go code that matches. If you want your database to drive your Go code - not the other way around - bob is the most direct path.
So where does kiln fit?
sqlc, Ent, and bob are all database access tools - they help you talk to your database from Go. You still build the API layer yourself: HTTP handlers, request validation, response serialisation, routing, pagination, filtering, error handling, OpenAPI specs.
For CRUD-heavy APIs, that boilerplate is where the time goes. That's the problem kiln solves.
kiln sits on top of bob. It uses bob's schema introspection to read your database, then generates the entire API layer above it:
┌─────────────────────────┐
HTTP / API │ │
(handlers, │ kiln generates │
routing, │ both layers │
OpenAPI) │ │
├─────────────────────────┤
Query layer │ bob (bundled by kiln) │
└────────────┬────────────┘
│
┌────────────┴────────────┐
Database │ PostgreSQL / MySQL / │
│ SQLite │
└─────────────────────────┘
From a schema, kiln gives you a running CRUD API - endpoints, validation, pagination, and an OpenAPI spec - without writing handlers by hand. Foreign keys become nested routes. Junction tables become link/unlink endpoints. You don't interact with bob directly - kiln handles the integration, but bob is doing the heavy lifting underneath.
kiln's limitations are real. It generates CRUD patterns only - if "publish post" is a state transition with side effects, kiln generates the update endpoint but the business logic is yours. If your API diverges from your database design (aggregations, denormalised responses, domain-driven endpoints), you write custom handlers. If most of your endpoints aren't CRUD-shaped, kiln will get in your way quickly.
They're not mutually exclusive
You can use kiln for your CRUD tables and sqlc for your complex reporting queries in the same project. The generated store uses bob's Executor interface, which wraps *sql.DB - you can share the same connection pool without conflicts.
A typical setup for a project with both CRUD and complex query needs:
- kiln + bob for the 80% of tables that are standard CRUD (users, teams, projects, settings)
- sqlc for the 20% that need hand-tuned queries (analytics dashboards, activity feeds, reporting)
Both tools read the same database. Both generate Go code. They complement each other.
Which one should you use?
It comes down to where you want to spend your time:
- Writing SQL? Use sqlc. Your queries are complex, hand-tuned, and you want zero runtime overhead.
- Modelling a graph? Use Ent. You have complex relationships, want built-in migrations, and prefer defining schemas in Go.
- Letting the database drive your code? Use bob. Your database is the source of truth and you want type-safe queries without writing SQL or a schema DSL.
- Shipping a CRUD API fast? Use kiln. You want a complete API from your schema - handlers, routing, validation, OpenAPI - not just a database client.
Real-world scenarios
- Internal admin tool - kiln
- SaaS backend with standard CRUD - kiln
- Analytics dashboard with complex queries - sqlc
- Social graph / deeply relational data - Ent
- Mixed project (CRUD + reporting) - kiln for the tables, sqlc for the queries
In one line: sqlc = query control. Ent = data modelling. bob = schema-first queries. kiln = schema-first API.
Next steps
- Browse the full documentation
- Browse kiln repository
- Try the demo blog API for a complete working example
- Follow the step-by-step tutorial: Generate a REST API from your Database Schema in Go