blob: cfaa3b16afe98e490cd0244d51298d7e5d8bf894 [file] [log] [blame]
Serge Bazanski1b28e1b2022-09-05 18:41:18 +02001load("@io_bazel_rules_go//go:def.bzl", "go_context")
2
3# _parse_migrations takes a list of golang-migrate compatible schema files (in
4# the format <timestamp>_some_description_{up,down}.sql) and splits them into
5# 'up' and 'down' dictionaries, each a map from timestamp to underlying file.
6#
7# It also does some checks on the provided file names, making sure that
8# golang-migrate will parse them correctly.
9def _parse_migrations(files):
10 uppers = {}
11 downers = {}
12
13 # Ensure filename fits golang-migrate format, sort into 'up' and 'down' files.
14 for file in files:
15 if not file.basename.endswith(".up.sql") and not file.basename.endswith(".down.sql"):
16 fail("migration %s must end woth .{up,down}.sql" % file.basename)
Tim Windelschmidt156248b2025-01-10 00:27:45 +010017 if len(file.basename.split(".")) != 3:
Serge Bazanski1b28e1b2022-09-05 18:41:18 +020018 fail("migration %s must not contain any . other than in .{up,down}.sql extension" % file.basename)
Tim Windelschmidt156248b2025-01-10 00:27:45 +010019 first = file.basename.split(".")[0]
20 if len(first.split("_")) < 2:
Serge Bazanski1b28e1b2022-09-05 18:41:18 +020021 fail("migration %s must be in <timestamp>_<name>.{up,down}.sql format" % file.basename)
Tim Windelschmidt156248b2025-01-10 00:27:45 +010022 timestamp = first.split("_")[0]
Serge Bazanski1b28e1b2022-09-05 18:41:18 +020023 if not timestamp.isdigit():
24 fail("migration %s must be in <timestamp>_<name>.{up,down}.sql format" % file.basename)
25 timestamp = int(timestamp)
26 if timestamp < 1662136250:
27 fail("migration %s must be in <timestamp>_<name>.{up,down}.sql format" % file.basename)
28
Tim Windelschmidt156248b2025-01-10 00:27:45 +010029 if file.basename.endswith(".up.sql"):
Serge Bazanski1b28e1b2022-09-05 18:41:18 +020030 if timestamp in uppers:
Tim Windelschmidt156248b2025-01-10 00:27:45 +010031 fail("migration %s conflicts with %s" % [file.basename, uppers[timestamp].basename])
Serge Bazanski1b28e1b2022-09-05 18:41:18 +020032 uppers[timestamp] = file
Tim Windelschmidt156248b2025-01-10 00:27:45 +010033 if file.basename.endswith(".down.sql"):
Serge Bazanski1b28e1b2022-09-05 18:41:18 +020034 if timestamp in downers:
Tim Windelschmidt156248b2025-01-10 00:27:45 +010035 fail("migration %s conflicts with %s" % [file.basename, downers[timestamp].basename])
Serge Bazanski1b28e1b2022-09-05 18:41:18 +020036 downers[timestamp] = file
37
38 # Check each 'up' has a corresponding 'down', and vice-versa.
39 for timestamp, up in uppers.items():
40 if timestamp not in downers:
41 fail("%s has no corresponding 'down' migration" % up.basename)
Tim Windelschmidt156248b2025-01-10 00:27:45 +010042 if downers[timestamp].basename.replace("down.sql", "up.sql") != up.basename:
Serge Bazanski1b28e1b2022-09-05 18:41:18 +020043 fail("%s has no corresponding 'down' migration" % up.basename)
44 for timestamp, down in downers.items():
45 if timestamp not in uppers:
46 fail("%s has no corresponding 'up' migration" % down.basename)
Tim Windelschmidt156248b2025-01-10 00:27:45 +010047 if uppers[timestamp].basename.replace("up.sql", "down.sql") != down.basename:
Serge Bazanski1b28e1b2022-09-05 18:41:18 +020048 fail("%s has no corresponding 'up' migration" % down.basename)
49
50 return uppers, downers
51
52def _sqlc_go_library(ctx):
53 go = go_context(ctx)
54
55 importpath_parts = ctx.attr.importpath.split("/")
56 package_name = importpath_parts[-1]
57
58 # Split migrations into 'up' and 'down'. Only pass 'up' to sqlc. Use both
59 # to generate golang-migrate compatible bindata.
Tim Windelschmidt156248b2025-01-10 00:27:45 +010060 uppers, _ = _parse_migrations(ctx.files.migrations)
Serge Bazanski1b28e1b2022-09-05 18:41:18 +020061
62 # Make sure given queries have no repeating basenames. This ensures clean
63 # mapping source SQL file name and generated Go file.
64 query_basenames = []
65 for query in ctx.files.queries:
66 if query.basename in query_basenames:
67 fail("duplicate %s base name in query files" % query.basename)
68 query_basenames.append(query.basename)
69
70 # Go files generated by sqlc.
71 sqlc_go_sources = [
72 # db.go and models.go always exist.
73 ctx.actions.declare_file("db.go"),
74 ctx.actions.declare_file("models.go"),
75 ]
Tim Windelschmidt156248b2025-01-10 00:27:45 +010076
Serge Bazanski1b28e1b2022-09-05 18:41:18 +020077 # For every query file, basename.go is also generated.
78 for basename in query_basenames:
79 sqlc_go_sources.append(ctx.actions.declare_file(basename + ".go"))
80
Serge Bazanski1b28e1b2022-09-05 18:41:18 +020081 # Cockroachdb is PostgreSQL with some extra overrides to fix Go/SQL type
82 # mappings.
83 overrides = []
84 if ctx.attr.dialect == "cockroachdb":
85 overrides = [
86 # INT is 64-bit in cockroachdb (32-bit in postgres).
Tim Windelschmidt156248b2025-01-10 00:27:45 +010087 {"go_type": "int64", "db_type": "pg_catalog.int4"},
Serge Bazanski1b28e1b2022-09-05 18:41:18 +020088 ]
89
90 config = ctx.actions.declare_file("_config.yaml")
Tim Windelschmidt156248b2025-01-10 00:27:45 +010091
Serge Bazanski1b28e1b2022-09-05 18:41:18 +020092 # All paths in config are relative to the config file. However, Bazel paths
93 # are relative to the execution root/CWD. To make things work regardless of
94 # config file placement, we prepend all config paths with a `../../ ...`
95 # path walk that makes the path be execroot relative again.
Tim Windelschmidt156248b2025-01-10 00:27:45 +010096 config_walk = "../" * config.path.count("/")
Serge Bazanski1b28e1b2022-09-05 18:41:18 +020097 config_data = json.encode({
98 "version": 2,
99 "sql": [
100 {
101 "schema": [config_walk + up.path for up in uppers.values()],
102 "queries": [config_walk + query.path for query in ctx.files.queries],
103 "engine": "postgresql",
104 "gen": {
105 "go": {
106 "package": package_name,
107 "out": config_walk + sqlc_go_sources[0].dirname,
108 "overrides": overrides,
109 },
110 },
111 },
112 ],
113 })
114 ctx.actions.write(config, config_data)
115
116 # Generate types/functions using sqlc.
117 ctx.actions.run(
118 mnemonic = "SqlcGen",
119 executable = ctx.executable._sqlc,
120 arguments = [
121 "generate",
Tim Windelschmidt156248b2025-01-10 00:27:45 +0100122 "-f",
123 config.path,
Serge Bazanski1b28e1b2022-09-05 18:41:18 +0200124 ],
125 inputs = [
Tim Windelschmidt156248b2025-01-10 00:27:45 +0100126 config,
Serge Bazanski1b28e1b2022-09-05 18:41:18 +0200127 ] + uppers.values() + ctx.files.queries,
128 outputs = sqlc_go_sources,
129 )
130
Serge Bazanski9cdec582022-09-15 18:48:27 +0200131 library = go.new_library(go, srcs = sqlc_go_sources, importparth = ctx.attr.importpath)
Serge Bazanski1b28e1b2022-09-05 18:41:18 +0200132 source = go.library_to_source(go, ctx.attr, library, ctx.coverage_instrumented())
133 return [
134 library,
135 source,
136 OutputGroupInfo(go_generated_srcs = depset(library.srcs)),
137 ]
138
Serge Bazanski1b28e1b2022-09-05 18:41:18 +0200139sqlc_go_library = rule(
140 implementation = _sqlc_go_library,
141 attrs = {
142 "migrations": attr.label_list(
143 allow_files = True,
144 ),
145 "queries": attr.label_list(
146 allow_files = True,
147 ),
148 "importpath": attr.string(
149 mandatory = True,
150 ),
151 "dialect": attr.string(
152 mandatory = True,
153 values = ["postgresql", "cockroachdb"],
154 ),
155 "_sqlc": attr.label(
Tim Windelschmidt3fdaeac2023-11-13 23:33:07 +0100156 default = Label("@com_github_sqlc_dev_sqlc//cmd/sqlc"),
Serge Bazanski1b28e1b2022-09-05 18:41:18 +0200157 allow_single_file = True,
158 executable = True,
159 cfg = "exec",
160 ),
161 "_bindata": attr.label(
162 default = Label("@com_github_kevinburke_go_bindata//go-bindata"),
163 allow_single_file = True,
164 executable = True,
165 cfg = "exec",
166 ),
167 },
168 toolchains = ["@io_bazel_rules_go//go:toolchain"],
169)