blob: 88b2ec578015d5dbba0e7b2bd071771af1ce7871 [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
Tim Windelschmidtbdd0d252025-01-09 22:31:08 +010058 # Split migrations into 'up' and 'down'. Only pass 'up' to sqlc.
Tim Windelschmidt156248b2025-01-10 00:27:45 +010059 uppers, _ = _parse_migrations(ctx.files.migrations)
Serge Bazanski1b28e1b2022-09-05 18:41:18 +020060
61 # Make sure given queries have no repeating basenames. This ensures clean
62 # mapping source SQL file name and generated Go file.
63 query_basenames = []
64 for query in ctx.files.queries:
65 if query.basename in query_basenames:
66 fail("duplicate %s base name in query files" % query.basename)
67 query_basenames.append(query.basename)
68
69 # Go files generated by sqlc.
70 sqlc_go_sources = [
71 # db.go and models.go always exist.
72 ctx.actions.declare_file("db.go"),
73 ctx.actions.declare_file("models.go"),
74 ]
Tim Windelschmidt156248b2025-01-10 00:27:45 +010075
Serge Bazanski1b28e1b2022-09-05 18:41:18 +020076 # For every query file, basename.go is also generated.
77 for basename in query_basenames:
78 sqlc_go_sources.append(ctx.actions.declare_file(basename + ".go"))
79
Serge Bazanski1b28e1b2022-09-05 18:41:18 +020080 # Cockroachdb is PostgreSQL with some extra overrides to fix Go/SQL type
81 # mappings.
82 overrides = []
83 if ctx.attr.dialect == "cockroachdb":
84 overrides = [
85 # INT is 64-bit in cockroachdb (32-bit in postgres).
Tim Windelschmidt156248b2025-01-10 00:27:45 +010086 {"go_type": "int64", "db_type": "pg_catalog.int4"},
Serge Bazanski1b28e1b2022-09-05 18:41:18 +020087 ]
88
89 config = ctx.actions.declare_file("_config.yaml")
Tim Windelschmidt156248b2025-01-10 00:27:45 +010090
Serge Bazanski1b28e1b2022-09-05 18:41:18 +020091 # All paths in config are relative to the config file. However, Bazel paths
92 # are relative to the execution root/CWD. To make things work regardless of
93 # config file placement, we prepend all config paths with a `../../ ...`
94 # path walk that makes the path be execroot relative again.
Tim Windelschmidt156248b2025-01-10 00:27:45 +010095 config_walk = "../" * config.path.count("/")
Serge Bazanski1b28e1b2022-09-05 18:41:18 +020096 config_data = json.encode({
97 "version": 2,
98 "sql": [
99 {
100 "schema": [config_walk + up.path for up in uppers.values()],
101 "queries": [config_walk + query.path for query in ctx.files.queries],
102 "engine": "postgresql",
103 "gen": {
104 "go": {
105 "package": package_name,
106 "out": config_walk + sqlc_go_sources[0].dirname,
107 "overrides": overrides,
108 },
109 },
110 },
111 ],
112 })
113 ctx.actions.write(config, config_data)
114
115 # Generate types/functions using sqlc.
116 ctx.actions.run(
117 mnemonic = "SqlcGen",
118 executable = ctx.executable._sqlc,
119 arguments = [
120 "generate",
Tim Windelschmidt156248b2025-01-10 00:27:45 +0100121 "-f",
122 config.path,
Serge Bazanski1b28e1b2022-09-05 18:41:18 +0200123 ],
124 inputs = [
Tim Windelschmidt156248b2025-01-10 00:27:45 +0100125 config,
Serge Bazanski1b28e1b2022-09-05 18:41:18 +0200126 ] + uppers.values() + ctx.files.queries,
127 outputs = sqlc_go_sources,
128 )
129
Serge Bazanski9cdec582022-09-15 18:48:27 +0200130 library = go.new_library(go, srcs = sqlc_go_sources, importparth = ctx.attr.importpath)
Serge Bazanski1b28e1b2022-09-05 18:41:18 +0200131 source = go.library_to_source(go, ctx.attr, library, ctx.coverage_instrumented())
132 return [
133 library,
134 source,
135 OutputGroupInfo(go_generated_srcs = depset(library.srcs)),
136 ]
137
Serge Bazanski1b28e1b2022-09-05 18:41:18 +0200138sqlc_go_library = rule(
139 implementation = _sqlc_go_library,
140 attrs = {
141 "migrations": attr.label_list(
142 allow_files = True,
143 ),
144 "queries": attr.label_list(
145 allow_files = True,
146 ),
147 "importpath": attr.string(
148 mandatory = True,
149 ),
150 "dialect": attr.string(
151 mandatory = True,
152 values = ["postgresql", "cockroachdb"],
153 ),
154 "_sqlc": attr.label(
Tim Windelschmidt25e0d8f2024-12-02 23:46:24 +0100155 default = Label(":sqlc"),
Serge Bazanski1b28e1b2022-09-05 18:41:18 +0200156 allow_single_file = True,
157 executable = True,
158 cfg = "exec",
159 ),
Serge Bazanski1b28e1b2022-09-05 18:41:18 +0200160 },
161 toolchains = ["@io_bazel_rules_go//go:toolchain"],
162)