Skip to main content

VillageSQL is a drop-in replacement for MySQL with extensions.

All examples in this guide work on VillageSQL. Install Now →
sqlc generates Go structs and query methods directly from SQL, so your application code calls plain Go functions instead of building queries at runtime. There is no ORM layer between your code and the SQL you wrote. This guide sets up a small posts table, generates code with sqlc against MySQL, and runs it against a live server.

Installing sqlc

Install sqlc with Homebrew, or with go install if you prefer to manage it as a Go tool:
Confirm the version:

Project Layout

Create a Go module and three files: a schema, a query file, and an sqlc configuration file.
schema.sql defines the table sqlc will generate types from. body has no NOT NULL constraint, which matters later:

Writing Annotated Queries

sqlc reads plain SQL files and turns a -- name: comment above each statement into a generated Go method. The word after the query name sets the return shape: :one for a single row, :many for a slice, :exec for no return value, :execresult for the raw sql.Result, and :execrows for the affected row count. MySQL’s INSERT statements do not support RETURNING, so getting a new row’s ID back needs a dedicated annotation. :execlastid calls sql.Result.LastInsertId() for you and returns it directly:

Configuring sqlc.yaml

sqlc.yaml points sqlc at the schema and query files and sets the MySQL engine. Paths are resolved relative to the location of sqlc.yaml, not the directory you run sqlc from:
package and out name the generated Go package and the directory it is written to. sql_package: "database/sql" tells sqlc to generate code against the standard library’s database/sql, which is what go-sql-driver/mysql implements.

Generating Code

This produces three files in posts/: db.go (the Queries struct and its constructor), models.go (the Post struct), and query.sql.go (one method per annotated query). models.go is where the nullable body column shows up:

Nullable Columns Generate sql.NullString, Not a Pointer

Because body TEXT has no NOT NULL constraint, sqlc generated sql.NullString for it rather than a plain string or a *string. The same rule applies to CreatePostParams, generated from the INSERT statement’s parameter list:
This means every insert has to construct a sql.NullString explicitly, even to pass a value:
To insert a row with no body, set Valid: false rather than leaving the field as an empty string. An empty sql.NullString{} zero value already has Valid: false, so it is equivalent to writing sql.NullString{Valid: false} explicitly:
Reading a nullable column back requires checking .Valid before trusting .String:
If a column needs to allow NULL, make that a deliberate schema decision. Adding NOT NULL to body in schema.sql and re-running sqlc generate changes the generated field from sql.NullString to a plain string everywhere it appears, which removes the .Valid check from every call site.

Connecting and Running Migrations

Create the database and an application user with a real password, scoped to that one database:
Apply schema.sql with the mysql client before running the application:
Add the driver and connect over TCP with parseTime=true, which is required for MySQL’s TIMESTAMP and DATETIME columns to scan into Go’s time.Time instead of []byte:

CreatePost Returns int64, GetPost Takes uint64

id BIGINT UNSIGNED maps to uint64 everywhere sqlc generates it as a column, including the id parameter of GetPost. But CreatePost, generated with :execlastid, returns a plain int64. That comes from sql.Result.LastInsertId(), which the standard library defines as int64 regardless of the column’s actual signedness. Passing a freshly created ID into GetPost needs an explicit conversion:
Running the full program end to end against a live server:
Checked directly against the database, independent of the program’s own output:

Tuning the Connection Pool

database/sql manages its own connection pool underneath sql.Open, and sqlc’s generated Queries struct uses whatever pool the *sql.DB you pass it already has. Two settings matter for a MySQL backend specifically:
SetConnMaxLifetime should be set below MySQL’s wait_timeout, which defaults to 28800 seconds (8 hours) on this server. Without it, connections that MySQL has already closed server-side stay in the Go pool as idle entries until a query against one fails.

Frequently Asked Questions

Does sqlc run against a live database?

Only when you explicitly enable it. sqlc generate parses schema.sql and query.sql statically and does not need a running server. sqlc vet and sqlc push can connect to a real database for additional checks, but the basic generate workflow in this guide never opens a connection.

What happens if a query in query.sql has a syntax error?

sqlc generate fails with a parse error naming the file and line before generating anything. Nothing is written to the output directory on failure, so a broken query never produces stale or partial generated code.

Can sqlc generate code for INSERT … ON DUPLICATE KEY UPDATE?

Yes. sqlc parses it as a normal INSERT statement. Annotate it with :exec, :execresult, :execrows, or :execlastid depending on which part of the result you need, the same as any other insert.

Troubleshooting

See also