Generate code with SQLC in Bazel
How to use SQLC to generate code in a Bazel setup
Bazel (checkout also their new docs) is a build system that advertises itself with
{ Fast, Correct } — Choose two
and that is absolutely correct, as long as we’re talking about execution time, not (necessarily) engineering time 😄
I’ve been trying to learn Bazel for a few months. To achieve this, I’m updating most of my projects to build with Bazel. In my spare time, I work on various small projects, mostly using Go. These range from command-line applications to server apps running in containers, and even a custom GitHub action for installing Hugo.
Refactoring these projects makes me consider various build tasks:
- building and pushing container images
- creating binary archives and attaching them to releases
- generating changelogs
- generating code
The problem
Today’s article is about generating code and especially generating code with sqlc.
You might be thinking:
That doesn’t sound so difficult, does it? Bazel has
genrule, Go 1.24 offers tool support, andrules_gosupports it too. Just do it!
Well, yes and no.
As usual you would start the journey with:1
bazel run @rules_go//go - get -tool github.com/sqlc-dev/sqlc/cmd/sqlc
to get the necessary tool dependency into your go.mod.
The next step is then of course to create a genrule to run sqlc and generate your code:
filegroup(
name = "sqlc_config",
srcs = ["sqlc.yml"],
)
filegroup(
name = "queries_and_schema",
srcs = glob(["*.sql"]),
)
genrule(
name = "sqlc_code",
srcs = [
":sqlc_config",
":queries_and_schema",
],
outs = [
"db.go",
"models.go",
"queries.postgres.sql.go",
],
cmd = """
$(execpath @com_github_sqlc_dev_sqlc//cmd/sqlc:sqlc) generate --file sqlc.yml
# this is actually only necessary if you generate the code in a sub-directory
mv {db,models,queries.postgres.sql}.go $(RULEDIR)/
""",
tools = [
"@com_github_sqlc_dev_sqlc//cmd/sqlc",
],
)
Remark: For simplicity, I will not elaborate on the structure of sqlc.yml or how to read your schema.
So, we’re done, yes? Not quite, of course we want to run this at least once so we can check whether the code is generated properly:
# assuming that the above snippet is in your root BUILD.bazel
bazel build //:sqlc_code
When executing this, you should be greeted by some error message like this:
ERROR: no such package '@@gazelle++go_deps+com_github_pingcap_tidb_pkg_parser//pkg/parser/terror': BUILD file not found in directory 'pkg/parser/terror' of external repository @@gazelle++go_deps+com_github_pingcap_tidb_pkg_parser. Add a BUILD file to a directory to mark it as a package.
You might be familiar with errors like this and the fix for this is rather straight forward:
go_deps.gazelle_override(
build_file_generation = "on",
path = "github.com/pingcap/tidb/pkg/parser",
)
okay, so now again, let’s generate our sqlc code!
ERROR: $HOME/.cache/bazel/_bazel_vscode/a36b5bd059192fed81ab47c4aa443c83/external/gazelle++go_deps+com_github_pganalyze_pg_query_go_v6/parser/BUILD.bazel:3:11: GoCompilePkg external/gazelle++go_deps+com_github_pganalyze_pg_query_go_v6/parser/parser.a [for tool] failed: (Exit 1): builder failed: error executing GoCompilePkg command (from target @@gazelle++go_deps+com_github_pganalyze_pg_query_go_v6//parser:parser) bazel-out/aarch64-opt-exec-ST-d57f47055a04/bin/external/rules_go++go_sdk+main___download_0/builder_reset/builder compilepkg -sdk external/rules_go++go_sdk+main___download_0 -goroot ... (remaining 197 arguments skipped)
Use --sandbox_debug to see verbose messages from the sandbox and retain the sandbox build root for debugging
external/gazelle++go_deps+com_github_pganalyze_pg_query_go_v6/parser/parser.go:7:10: fatal error: pg_query.h: No such file or directory
7 | #include "pg_query.h"
| ^~~~~~~~~~~~
compilation terminated.
compilepkg: error running subcommand external/rules_go++go_sdk+main___download_0/pkg/tool/linux_arm64/cgo
Obviously! Actually, if you directly know what’s going on here, I’ll take my hat off to you! I mean, yes, it is relatively obvious that there’s some kind of cgo issue and that it cannot resolve some include but how the hack am I supposed to solve this if this is deep in the dependency chain?!
The solution
Fortunately, there’s an issue in the sqlc repository that discusses this very problem, although for a much older version (v2 instead of v6).
So, we can simply take the solution and “quickly” adapt it, right?
Before we get to that, let’s have a look at how the solution works:
- create a patch that updates the generated include statement, create a
cc_librarythat references all headers and and includes thiscc_libraryviacdeps - explicitly reference the repository in question and apply the aforementioned patch
If you look closer at the patch, you will notice that the patch is for version v2 of the library, so we have to make sure this works the same way for v62.
Also in 2025 we’re not using go_repository anymore, because with Bzlmod everything works a little bit differently 😅
Patch
Let’s start with the patch, in the beginning I was wondering, how I should be able to patch something I do not even know how it looks like in the first place until I gave the error message above a closer look and realized, I absolutely know how the file looks like: $HOME/.cache/bazel/_bazel_vscode/a36b5bd059192fed81ab47c4aa443c83/external/gazelle++go_deps+com_github_pganalyze_pg_query_go_v6/parser/BUILD.bazel.
With the existing patch for the earlier version in the linked issue, it was fairly simply to update the patch accordingly.
The only major difference was that instead of -Iparser/include I had to patch -Iparser/include -Iparser/include/postgres and I also had to update the paths because apparently the gazelle internal paths changed. I couldn’t find an obvious and easy way to lookup this path in a reliable way so I decided, as long as it works, I’ll keep it. If you are aware of a better way, please let me know!
So, without further ado:
diff --git a/parser/BUILD.bazel b/parser/BUILD.bazel
--- parser/BUILD.bazel
+++ parser/BUILD.bazel
@@ -87,18 +88,24 @@
cgo = True,
clinkopts = [""],
copts = [
- "-Iparser/include -Iparser/include/postgres -g -fstack-protector -std=gnu99 -Wno-unknown-warning-option",
+ "-Iexternal/gazelle++go_deps+com_github_pganalyze_pg_query_go_v6/parser/include/ -Iexternal/gazelle++go_deps+com_github_pganalyze_pg_query_go_v6/parser/include/postgres -g -fstack-protector -std=gnu99 -Wno-unknown-warning-option",
] + select({
"@io_bazel_rules_go//go/platform:windows": [
"-Iparser/include/postgres/port/win32",
],
"//conditions:default": [],
}),
+ cdeps = [":include"],
importpath = "github.com/pganalyze/pg_query_go/v6/parser",
importpath_aliases = ["github.com/pganalyze/pg_query_go/parser"],
visibility = ["//visibility:public"],
)
+cc_library(
+ name = "include",
+ hdrs = glob(["include/**"]),
+)
+
alias(
name = "go_default_library",
actual = ":parser",
go_repository equivalent for Bzlmod
Instead of go_repository you can now use module_override as a function on use_extension("@rules_go//go:extensions.bzl", "go_sdk").
For example
go_sdk = use_extension("@rules_go//go:extensions.bzl", "go_sdk")
go_sdk.from_file(go_mod = "//:go.mod")
go_deps.module_override(
patches = [
"//:build/patches/pg_query_include.patch",
],
path = "github.com/pganalyze/pg_query_go/v6",
)
the label //:build/patches/pg_query_include.patch is defined as follows //:<path to patch file from repo root>.
To wrap it up:
go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps")
go_deps.from_file(go_mod = "//:go.mod")
go_deps.gazelle_override(
build_file_generation = "on",
path = "github.com/pingcap/tidb/pkg/parser",
)
go_deps.gazelle_override(
build_file_generation = "on",
directives = [
"gazelle:proto disable_global",
],
path = "github.com/sqlc-dev/sqlc",
)
go_deps.module_override(
patches = [
"//:build/patches/pg_query_include.patch",
],
path = "github.com/pganalyze/pg_query_go/v6",
)
Finally fixed all issues and I could generate the necessary code with sqlc and move on to write the actual code 😄