Serge Bazanski | 1b28e1b | 2022-09-05 18:41:18 +0200 | [diff] [blame^] | 1 | load("@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. |
| 9 | def _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) |
| 17 | if len(file.basename.split('.')) != 3: |
| 18 | fail("migration %s must not contain any . other than in .{up,down}.sql extension" % file.basename) |
| 19 | first = file.basename.split('.')[0] |
| 20 | if len(first.split('_')) < 2: |
| 21 | fail("migration %s must be in <timestamp>_<name>.{up,down}.sql format" % file.basename) |
| 22 | timestamp = first.split('_')[0] |
| 23 | 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 | |
| 29 | if file.basename.endswith('.up.sql'): |
| 30 | if timestamp in uppers: |
| 31 | fail("migration %s conflicts with %s" % [file.basename, uppers[timestamp].basename]) |
| 32 | uppers[timestamp] = file |
| 33 | if file.basename.endswith('.down.sql'): |
| 34 | if timestamp in downers: |
| 35 | fail("migration %s conflicts with %s" % [file.basename, downers[timestamp].basename]) |
| 36 | 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) |
| 42 | if downers[timestamp].basename.replace('down.sql', 'up.sql') != up.basename: |
| 43 | 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) |
| 47 | if uppers[timestamp].basename.replace('up.sql', 'down.sql') != down.basename: |
| 48 | fail("%s has no corresponding 'up' migration" % down.basename) |
| 49 | |
| 50 | return uppers, downers |
| 51 | |
| 52 | def _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. |
| 60 | uppers, downers = _parse_migrations(ctx.files.migrations) |
| 61 | |
| 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 | ] |
| 76 | # 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 | |
| 80 | migrations_source = ctx.actions.declare_file("migrations.go") |
| 81 | |
| 82 | # Cockroachdb is PostgreSQL with some extra overrides to fix Go/SQL type |
| 83 | # mappings. |
| 84 | overrides = [] |
| 85 | if ctx.attr.dialect == "cockroachdb": |
| 86 | overrides = [ |
| 87 | # INT is 64-bit in cockroachdb (32-bit in postgres). |
| 88 | { "go_type": "int64", "db_type": "pg_catalog.int4" }, |
| 89 | ] |
| 90 | |
| 91 | config = ctx.actions.declare_file("_config.yaml") |
| 92 | # 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. |
| 96 | config_walk = '../' * config.path.count('/') |
| 97 | 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", |
| 122 | "-f", config.path, |
| 123 | ], |
| 124 | inputs = [ |
| 125 | config |
| 126 | ] + uppers.values() + ctx.files.queries, |
| 127 | outputs = sqlc_go_sources, |
| 128 | ) |
| 129 | |
| 130 | # Generate migrations bindata. |
| 131 | ctx.actions.run( |
| 132 | mnemonic = "MigrationsEmbed", |
| 133 | executable = ctx.executable._bindata, |
| 134 | arguments = [ |
| 135 | "-pkg", package_name, |
| 136 | "-prefix", ctx.label.workspace_root, |
| 137 | "-o", migrations_source.path, |
| 138 | ] + [up.path for up in uppers.values()] + [down.path for down in downers.values()], |
| 139 | inputs = uppers.values() + downers.values(), |
| 140 | outputs = [migrations_source], |
| 141 | ) |
| 142 | |
| 143 | library = go.new_library(go, srcs = sqlc_go_sources + [migrations_source], importparth = ctx.attr.importpath) |
| 144 | source = go.library_to_source(go, ctx.attr, library, ctx.coverage_instrumented()) |
| 145 | return [ |
| 146 | library, |
| 147 | source, |
| 148 | OutputGroupInfo(go_generated_srcs = depset(library.srcs)), |
| 149 | ] |
| 150 | |
| 151 | |
| 152 | sqlc_go_library = rule( |
| 153 | implementation = _sqlc_go_library, |
| 154 | attrs = { |
| 155 | "migrations": attr.label_list( |
| 156 | allow_files = True, |
| 157 | ), |
| 158 | "queries": attr.label_list( |
| 159 | allow_files = True, |
| 160 | ), |
| 161 | "importpath": attr.string( |
| 162 | mandatory = True, |
| 163 | ), |
| 164 | "dialect": attr.string( |
| 165 | mandatory = True, |
| 166 | values = ["postgresql", "cockroachdb"], |
| 167 | ), |
| 168 | "_sqlc": attr.label( |
| 169 | default = Label("@com_github_kyleconroy_sqlc//cmd/sqlc"), |
| 170 | allow_single_file = True, |
| 171 | executable = True, |
| 172 | cfg = "exec", |
| 173 | ), |
| 174 | "_bindata": attr.label( |
| 175 | default = Label("@com_github_kevinburke_go_bindata//go-bindata"), |
| 176 | allow_single_file = True, |
| 177 | executable = True, |
| 178 | cfg = "exec", |
| 179 | ), |
| 180 | }, |
| 181 | toolchains = ["@io_bazel_rules_go//go:toolchain"], |
| 182 | ) |