anchor_migrations 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rubocop.yml +16 -0
- data/CHANGELOG.md +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +175 -0
- data/Rakefile +14 -0
- data/exe/anchor +7 -0
- data/lib/anchor_migrations/cli.rb +128 -0
- data/lib/anchor_migrations/configuration.rb +12 -0
- data/lib/anchor_migrations/generator.rb +34 -0
- data/lib/anchor_migrations/initializer_generator.rb +24 -0
- data/lib/anchor_migrations/rails_loader.rb +11 -0
- data/lib/anchor_migrations/rails_migration_generator.rb +104 -0
- data/lib/anchor_migrations/railtie.rb +14 -0
- data/lib/anchor_migrations/utility.rb +16 -0
- data/lib/anchor_migrations/version.rb +7 -0
- data/lib/anchor_migrations.rb +31 -0
- data/sig/anchor_migrations.rbs +4 -0
- metadata +64 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 886e3d44b9b0a3bc3147c4f4eca63e86047a9cb83bd8f69d54a7912b8e922608
|
4
|
+
data.tar.gz: ceb5f6cb450ff6971f12dc3d663d4ab002b35d7564db793842b3198e86cead7b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 00b8f6345f4ffe5ebb17d2df959696a408e6044109f7f185e6e525b515e9390fa0ac25b8dbb4a7a7969eb5795e352904ba2f32299a5e4cfbc60708153485f235
|
7
|
+
data.tar.gz: e22efbb42d0c23d7cadeed1a06212c49fe6603564d249e05b3aa3e0cf83fb328f5a3a1812c3c517c86fb6c80470ff8b222de026f539d4d77b085f5c16aa6bed6
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
AllCops:
|
2
|
+
TargetRubyVersion: 3.3
|
3
|
+
Exclude:
|
4
|
+
- 'db/migrate/**/*'
|
5
|
+
|
6
|
+
Style/StringLiterals:
|
7
|
+
EnforcedStyle: double_quotes
|
8
|
+
|
9
|
+
Style/StringLiteralsInInterpolation:
|
10
|
+
EnforcedStyle: double_quotes
|
11
|
+
|
12
|
+
Metrics/MethodLength:
|
13
|
+
Max: 40
|
14
|
+
|
15
|
+
Metrics/AbcSize:
|
16
|
+
Enabled: false
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
## [Unreleased]
|
2
|
+
|
3
|
+
## [0.1.5] - 2025-07-11
|
4
|
+
|
5
|
+
Anchors aweigh! This is a brand new gem with very limited usage. It's in an experimental status for the time being.
|
6
|
+
|
7
|
+
- 0.1.x to version preceding the one above were yanked due to bugs
|
8
|
+
- Initial release, tested end to end in a Rails app
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2025 Andrew Atkinson
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,175 @@
|
|
1
|
+
# ⚓ Anchor Migrations
|
2
|
+
Anchor Migrations are SQL DDL, non-blocking, idempotent, and augment the normal ORM migrations process.
|
3
|
+
|
4
|
+
## Commands
|
5
|
+
```sh
|
6
|
+
anchor init # initialize directories
|
7
|
+
anchor generate # generate an empty versioned .sql file, to be filled in
|
8
|
+
anchor lint # safety-lint all .sql files using Squawk
|
9
|
+
anchor backfill # Backfill a Rails migration from the SQL
|
10
|
+
anchor migrate # Run the Anchor Migration DDL
|
11
|
+
```
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
Add `anchor_migrations` to your `development` group Gemfile
|
15
|
+
```rb
|
16
|
+
group :development do
|
17
|
+
gem 'anchor_migrations'
|
18
|
+
end
|
19
|
+
```
|
20
|
+
Then run `bundle install`.
|
21
|
+
|
22
|
+
## Preconditions
|
23
|
+
Anchor Migrations are restricted and opinionated for now, expecting a few things:
|
24
|
+
- Postgres only, 13+
|
25
|
+
- `DATABASE_URL` environment variable is set to the database to migrate (e.g. production), and is reachable, in order to apply migrations
|
26
|
+
- `psql` client accessible in PATH
|
27
|
+
- [Squawk](https://squawkhq.com) executable ([Quick Start documentation](https://squawkhq.com/docs/)) accessible in PATH
|
28
|
+
|
29
|
+
## Safety linting and lock_timeout
|
30
|
+
Squawk is used on SQL migrations to check for unsafe operations. For example, creating an index or dropping an index without using CONCURRENTLY is detected by Squak. Anchor Migrations will require safety-linted SQL, although right now it’s up to the developer to run `anchor lint` in their workflow.
|
31
|
+
|
32
|
+
When Anchor Migration SQL is ready to apply, a psql client connection is used for that. By default a 2 second `lock_timeout`[^docs] is set.
|
33
|
+
|
34
|
+
## What problems do Anchor migrations solve?
|
35
|
+
1. Anchor Migrations are an additional mechanism to release safe DDL changes that don’t have code dependencies, while keeping all databases in sync using ORM migrations.
|
36
|
+
|
37
|
+
Anchor Migrations are a process for organizations not using Trunk Based Development[^tbd] or frequent releases, to allow safe DDL to get released more frequently.
|
38
|
+
|
39
|
+
Because Anchor Migrations generate the ORM (Active Record) migration *from* the SQL, Rails migrations stay in sync.
|
40
|
+
|
41
|
+
## Example Anchor Migration SQL
|
42
|
+
By default, Anchor Migrations are stored in `db/anchor_migrations` as `.sql` files:
|
43
|
+
```sql
|
44
|
+
-- anchor_migrations/20250623173850_create_index_tbl_col.sql
|
45
|
+
CREATE INDEX CONCURRENTLY IF NOT EXISTS
|
46
|
+
idx_trips_created_at ON trips (created_at);
|
47
|
+
```
|
48
|
+
|
49
|
+
Squawk runs on the SQL file above for "safety linting", looking for unsafe patterns.
|
50
|
+
|
51
|
+
Run it using anchor migrations with the `lint` command:
|
52
|
+
```sh
|
53
|
+
~/app ➜ bundle exec anchor lint
|
54
|
+
|
55
|
+
Found 0 issues in 1 file 🎉
|
56
|
+
```
|
57
|
+
|
58
|
+
## Example ORM (Active Record) Migration
|
59
|
+
This Rails migration was generated from the Anchor Migration SQL file above.
|
60
|
+
|
61
|
+
For this example, Strong Migrations is used, so the `safety_assured` block it expects is added to the Rails migration.
|
62
|
+
```rb
|
63
|
+
#
|
64
|
+
# ################################################
|
65
|
+
# DO NOT EDIT, generated by Anchor Migrations
|
66
|
+
# Version: 20250623173850
|
67
|
+
# File: anchor_migrations/20250623173850_anchor_migration.sql
|
68
|
+
# ################################################
|
69
|
+
#
|
70
|
+
class CreateIndexIdxTripsCreatedAt < ActiveRecord::Migration[7.2]
|
71
|
+
disable_ddl_transaction!
|
72
|
+
|
73
|
+
def change
|
74
|
+
safety_assured do
|
75
|
+
execute <<-SQL
|
76
|
+
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_trips_created_at ON trips (created_at);
|
77
|
+
SQL
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
```
|
82
|
+
|
83
|
+
## Configuration
|
84
|
+
Currently, limited configuration is supported.
|
85
|
+
|
86
|
+
Anchor Migrations supports generating Strong Migrations-compatible Active Record migrations:
|
87
|
+
```rb
|
88
|
+
# config/initializers/anchor_migrations.rb
|
89
|
+
AnchorMigrations.configure do |config|
|
90
|
+
config.use_strong_migrations = true
|
91
|
+
end
|
92
|
+
```
|
93
|
+
|
94
|
+
## Example PR Workflow
|
95
|
+
For a PR, add:
|
96
|
+
1. The Anchor Migration SQL file.
|
97
|
+
1. The normal Rails migration files: Run `db:migrate` like normal to apply the migration. The developer submits the migration file and the diff to `db/structure.sql` or `db/schema.rb` like they normally would.
|
98
|
+
1. Get an "approval" describing your plans to run `bundle exec anchor migrate`. The approval can be a comment in the PR from a team member.
|
99
|
+
1. With an approval, run `anchor migrate` and capture the output (see below). Once applied, move the SQL migration into `db/anchor_migrations/applied` and update your PR.
|
100
|
+
1. Get PR approval and merge it in. The Rails migration "backfills" the DDL, applying it wherever it's needed.
|
101
|
+
|
102
|
+
Example output of `bundle exec anchor migrate`:
|
103
|
+
```
|
104
|
+
Applying Version: 20250623173850
|
105
|
+
CREATE INDEX CONCURRENTLY IF NOT EXISTS
|
106
|
+
idx_trips_created_at ON trips (created_at);
|
107
|
+
STDOUT:
|
108
|
+
CREATE INDEX
|
109
|
+
STDERR:
|
110
|
+
Success!
|
111
|
+
Applied Version: 20250623173850
|
112
|
+
Exit code: 0
|
113
|
+
```
|
114
|
+
The idempotent Rails migration applies anywhere it's needed and the Rails app is deployed. Other developer databases, lower environments, CI, etc. When it reaches production, the Anchor Migration SQL DDL was already applied, so nothing happens. The Rails migration is idempotent.
|
115
|
+
|
116
|
+
## Good uses of Anchor Migrations
|
117
|
+
### Query support, data integrity, data quality
|
118
|
+
Indexes (and eventually constraints) that support query performance or data integrity, but have no code dependencies, can be changed at a faster cadence, while keeping everything consistent.
|
119
|
+
|
120
|
+
Indexes and constraints improve performance and data quality, and arguably shouldn’t be "blocked" by needing to wait for ORM migrations.
|
121
|
+
|
122
|
+
### Long running DDL changes
|
123
|
+
On large tables, creating indexes concurrently can take a long time. It's nice to perform that during a low activity period, requiring control over the timing, which isn't always possible with ORM migrations.
|
124
|
+
|
125
|
+
## Anchor Migrations Properties
|
126
|
+
### Idempotent
|
127
|
+
Anchor Migrations in SQL must be written using idempotent tactics like `IF NOT EXISTS`.
|
128
|
+
|
129
|
+
This allows the SQL to be the backfill source for an Active Record migration, which is then also idempotent.
|
130
|
+
|
131
|
+
### Restricted DDL: What DDL is supported for Anchor Migrations?
|
132
|
+
Only non-blocking, idempotent DDL is supported. This list is restricted heavily now although additional DDL types may be added in the future:
|
133
|
+
1. `CREATE INDEX CONCURRENTLY IF NOT EXISTS`
|
134
|
+
1. `DROP INDEX CONCURRENTLY IF EXISTS` (Postgres 13+)
|
135
|
+
|
136
|
+
Roadmap operations, future gem releases:
|
137
|
+
1. `ALTER TABLE ALTER COLUMN IF NOT EXISTS` (only `NULL` values)
|
138
|
+
1. Add check constraint, initially not valid
|
139
|
+
|
140
|
+
### Uses psql
|
141
|
+
For now, Anchor Migrations assumes you're using psql to migrate, and that psql can connect to your target instance.
|
142
|
+
|
143
|
+
### What’s out of scope for Anchor Migrations?
|
144
|
+
Anchor Migrations are non-blocking and idempotent.
|
145
|
+
|
146
|
+
For destructive operations like table drops, column drops, etc. with code dependencies, Anchor Migrations are not appropriate.
|
147
|
+
|
148
|
+
That's because application code references need to be removed first.
|
149
|
+
|
150
|
+
Use Strong Migrations or similar to help guide that process, and use regular Rails migrations.
|
151
|
+
|
152
|
+
Some of those destructive operations are:
|
153
|
+
1. `DROP TABLE`
|
154
|
+
1. Adding non-nullable column, or a column with a default value
|
155
|
+
1. Dropping constraints
|
156
|
+
1. Adding initially valid constraints
|
157
|
+
1. Add indexes without using concurrently
|
158
|
+
|
159
|
+
[^docs]: <https://www.postgresql.org/docs/current/runtime-config-client.html>
|
160
|
+
[^tbd]: <https://trunkbaseddevelopment.com>
|
161
|
+
|
162
|
+
## Building and Testing
|
163
|
+
```sh
|
164
|
+
gem build anchor_migrations.gemspec
|
165
|
+
gem install ./anchor_migrations-0.1.0.gem
|
166
|
+
bundle exec rake test
|
167
|
+
```
|
168
|
+
|
169
|
+
## Testing Integration in Rails
|
170
|
+
Add to the project's Gemfile, then run `bundle`.
|
171
|
+
|
172
|
+
Once installed, test that it works by running:
|
173
|
+
```sh
|
174
|
+
bundle exec anchor help
|
175
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "minitest/test_task"
|
5
|
+
|
6
|
+
Minitest::TestTask.create
|
7
|
+
|
8
|
+
# TODO: GitHub Workflow is hanging, trying to disable the RuboCop portion
|
9
|
+
# require "rubocop/rake_task"
|
10
|
+
#
|
11
|
+
# RuboCop::RakeTask.new
|
12
|
+
|
13
|
+
# task default: %i[test rubocop]
|
14
|
+
task default: %i[test]
|
data/exe/anchor
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fileutils"
|
4
|
+
require "uri"
|
5
|
+
require "open3"
|
6
|
+
|
7
|
+
module AnchorMigrations
|
8
|
+
# Process command line arguments
|
9
|
+
class CLI
|
10
|
+
def self.start(args)
|
11
|
+
command = args.shift
|
12
|
+
|
13
|
+
case command
|
14
|
+
when "help"
|
15
|
+
new.help
|
16
|
+
when "init"
|
17
|
+
new.init
|
18
|
+
when "generate"
|
19
|
+
new.generate
|
20
|
+
when "lint"
|
21
|
+
new.lint
|
22
|
+
when "backfill"
|
23
|
+
new.generate_rails_migration
|
24
|
+
when "migrate"
|
25
|
+
new.migrate
|
26
|
+
else
|
27
|
+
new.help
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def init
|
32
|
+
puts "Initializing anchor migrations structure..."
|
33
|
+
|
34
|
+
folders = [
|
35
|
+
AnchorMigrations::DEFAULT_DIR,
|
36
|
+
"#{AnchorMigrations::DEFAULT_DIR}/applied",
|
37
|
+
"config/initializers",
|
38
|
+
"db/migrate"
|
39
|
+
]
|
40
|
+
|
41
|
+
folders.each do |folder|
|
42
|
+
if Dir.exist?(folder)
|
43
|
+
puts "Directory #{folder} already exists"
|
44
|
+
else
|
45
|
+
FileUtils.mkdir_p(folder)
|
46
|
+
puts "Created directory #{folder}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
puts "Checking for Squawk"
|
51
|
+
check_for_squawk
|
52
|
+
|
53
|
+
puts "Adding initializer"
|
54
|
+
InitializerGenerator.new.generate
|
55
|
+
|
56
|
+
puts "Anchor migrations structure initialized."
|
57
|
+
end
|
58
|
+
|
59
|
+
def generate
|
60
|
+
# TODO: accept file name argument
|
61
|
+
Generator.new.generate
|
62
|
+
end
|
63
|
+
|
64
|
+
def lint
|
65
|
+
unless Dir.exist?(AnchorMigrations::DEFAULT_DIR)
|
66
|
+
abort "Error: '#{AnchorMigrations::DEFAULT_DIR}' not found. Did you run anchor init?"
|
67
|
+
end
|
68
|
+
check_for_squawk
|
69
|
+
system("squawk lint #{AnchorMigrations::DEFAULT_DIR}/*.sql")
|
70
|
+
end
|
71
|
+
|
72
|
+
def migrate
|
73
|
+
# TODO: Only supporting one file for now. Expecting files to be moved into "applied" directory.
|
74
|
+
anchor_migration_file = Dir["#{AnchorMigrations::DEFAULT_DIR}/*.sql"].max
|
75
|
+
version = File.basename(anchor_migration_file).split("_").first
|
76
|
+
sql_ddl = File.read(anchor_migration_file)
|
77
|
+
cleaned_sql = AnchorMigrations::Utility.cleaned_sql(sql_ddl)
|
78
|
+
puts "Applying Version: #{version}"
|
79
|
+
puts cleaned_sql
|
80
|
+
|
81
|
+
if !ENV["DATABASE_URL"]
|
82
|
+
puts "DATABASE_URL must be set...exiting"
|
83
|
+
exit 1
|
84
|
+
else
|
85
|
+
base_url = URI.parse(ENV["DATABASE_URL"])
|
86
|
+
params = {
|
87
|
+
options: "-c lock_timeout=2s"
|
88
|
+
}
|
89
|
+
encoded = URI.encode_www_form(params)
|
90
|
+
encoded = encoded.gsub("+", "%20") # remove "+" for Postgres
|
91
|
+
conn_string = "#{base_url}?#{encoded}"
|
92
|
+
command = %(
|
93
|
+
psql #{conn_string} \
|
94
|
+
-c '#{cleaned_sql}'
|
95
|
+
)
|
96
|
+
stdout, stderr, status = Open3.capture3(command)
|
97
|
+
puts "STDOUT:\n#{stdout}"
|
98
|
+
puts "STDERR:\n#{stderr}"
|
99
|
+
if status.success?
|
100
|
+
puts "Success!"
|
101
|
+
puts "Applied Version: #{version}"
|
102
|
+
end
|
103
|
+
puts "Exit code: #{status.exitstatus}"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def generate_rails_migration
|
108
|
+
RailsMigrationGenerator.new.generate
|
109
|
+
end
|
110
|
+
|
111
|
+
def help
|
112
|
+
puts "Available commands: init, generate, lint, backfill, migrate"
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def check_for_squawk
|
118
|
+
if !system("which squawk > /dev/null 2>&1")
|
119
|
+
abort <<~MSG
|
120
|
+
"Error: 'squawk' command not found in PATH."
|
121
|
+
Is it installed? https://squawkhq.com/docs/
|
122
|
+
MSG
|
123
|
+
else
|
124
|
+
puts "Squawk found."
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnchorMigrations
|
4
|
+
# Generate Anchor Migrations SQL files
|
5
|
+
class Generator
|
6
|
+
def anchor_migrations_dir
|
7
|
+
File.join(AnchorMigrations::DEFAULT_DIR)
|
8
|
+
end
|
9
|
+
|
10
|
+
def generate
|
11
|
+
output_file = "#{anchor_migrations_dir}/#{rails_style_timestamp}_anchor_migration.sql"
|
12
|
+
File.write(output_file, sql_migration_template)
|
13
|
+
puts "Wrote file: #{output_file}"
|
14
|
+
puts File.read(output_file)
|
15
|
+
end
|
16
|
+
|
17
|
+
def rails_style_timestamp
|
18
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
19
|
+
end
|
20
|
+
|
21
|
+
def sql_migration_template
|
22
|
+
<<~SQL_TEMPLATE.strip
|
23
|
+
-- Generated by anchor_migrations #{VERSION}
|
24
|
+
--
|
25
|
+
-- Examples:
|
26
|
+
-- CREATE INDEX CONCURRENTLY IF NOT EXISTS
|
27
|
+
-- idx_tbl_col ON tbl (col);
|
28
|
+
--
|
29
|
+
-- DROP INDEX CONCURRENTLY IF EXISTS
|
30
|
+
-- idx_tbl_col;
|
31
|
+
SQL_TEMPLATE
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnchorMigrations
|
4
|
+
# Create a Rails migration from Anchor Migration SQL
|
5
|
+
class InitializerGenerator
|
6
|
+
def generate
|
7
|
+
filename = "anchor_migrations.rb"
|
8
|
+
file = "config/initializers/#{filename}"
|
9
|
+
return if File.exist?(file)
|
10
|
+
|
11
|
+
File.write(file, initalizer_template)
|
12
|
+
puts "Wrote file: #{file}"
|
13
|
+
puts File.read(file)
|
14
|
+
end
|
15
|
+
|
16
|
+
def initalizer_template
|
17
|
+
<<~TEMPLATE.strip
|
18
|
+
AnchorMigrations.configure do |config|
|
19
|
+
config.use_strong_migrations = false
|
20
|
+
end
|
21
|
+
TEMPLATE
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnchorMigrations
|
4
|
+
# Load the Rails environment
|
5
|
+
module RailsLoader
|
6
|
+
def self.load_rails!
|
7
|
+
env_path = File.expand_path("config/environment", Dir.pwd)
|
8
|
+
require "#{env_path}.rb" if File.exist?("#{env_path}.rb")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnchorMigrations
|
4
|
+
#
|
5
|
+
# Create a Rails migration from Anchor Migration SQL
|
6
|
+
# Expects Anchor Migrations to be in db/anchor_migrations
|
7
|
+
# Expects to generate Rails Migrations into db/migrate
|
8
|
+
# Expects to parse a Rails-style timestamp from beginning of Anchor Migrations file
|
9
|
+
#
|
10
|
+
class RailsMigrationGenerator
|
11
|
+
def initialize(anchor_migrations_dir: AnchorMigrations::DEFAULT_DIR)
|
12
|
+
if @anchor_migration_file = Dir["#{anchor_migrations_dir}/*.sql"].max
|
13
|
+
@migration_version = File.basename(@anchor_migration_file).split("_").first
|
14
|
+
sql_file_content = File.read(@anchor_migration_file)
|
15
|
+
@cleaned_sql_ddl = AnchorMigrations::Utility.cleaned_sql(sql_file_content)
|
16
|
+
build_file_name_and_class_name
|
17
|
+
AnchorMigrations::RailsLoader.load_rails!
|
18
|
+
else
|
19
|
+
abort "Can't find dir #{anchor_migrations_dir} or file #{Dir["#{anchor_migrations_dir}/*.sql"]}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def generate(write_file: true)
|
24
|
+
output_file = "#{AnchorMigrations::RAILS_MIG_DIR}/#{@migration_version}_#{@migration_file_name_no_version}"
|
25
|
+
migration_content = rails_generate_migration_code
|
26
|
+
if write_file
|
27
|
+
File.write(output_file, migration_content)
|
28
|
+
puts "Wrote file: #{output_file}"
|
29
|
+
puts File.read(output_file)
|
30
|
+
end
|
31
|
+
migration_content # for tests
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def rails_version_major_minor
|
37
|
+
if defined?(Rails) && defined?(Rails::VERSION)
|
38
|
+
major = Rails::VERSION::MAJOR
|
39
|
+
minor = Rails::VERSION::MINOR
|
40
|
+
"#{major}.#{minor}"
|
41
|
+
else
|
42
|
+
"X.Y" # can't load Rails, this is only for testing outside Rails
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def build_file_name_and_class_name
|
47
|
+
# Strip out the Rails-style version number
|
48
|
+
@migration_version = File.basename(@anchor_migration_file).split("_").first
|
49
|
+
|
50
|
+
# Break up by underscore, and join together with underscore, for only the basename
|
51
|
+
path = File.basename(@anchor_migration_file)
|
52
|
+
.split("_")[1..-1].join("_")
|
53
|
+
|
54
|
+
# Strip out the extension (.sql)
|
55
|
+
filename_base = File.basename(path, File.extname(path))
|
56
|
+
|
57
|
+
# Create a capitalized words version for the migration class name
|
58
|
+
@migration_class_name = filename_base.split("_")
|
59
|
+
.map(&:capitalize).join
|
60
|
+
|
61
|
+
# Create a filename with .rb extension
|
62
|
+
@migration_file_name_no_version = "#{filename_base}.rb"
|
63
|
+
end
|
64
|
+
|
65
|
+
def migration_change_method_body
|
66
|
+
if AnchorMigrations.configuration.use_strong_migrations
|
67
|
+
<<-TEMPL
|
68
|
+
safety_assured do
|
69
|
+
execute <<-SQL
|
70
|
+
#{@cleaned_sql_ddl}
|
71
|
+
SQL
|
72
|
+
end
|
73
|
+
TEMPL
|
74
|
+
else
|
75
|
+
<<-TEMPL
|
76
|
+
execute <<-SQL
|
77
|
+
#{@cleaned_sql_ddl}
|
78
|
+
SQL
|
79
|
+
TEMPL
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Assume it's a concurrently operation for now, disable_ddl_transaction!
|
84
|
+
def rails_generate_migration_code
|
85
|
+
<<~MIG_TEMPLATE.strip
|
86
|
+
#
|
87
|
+
# ################################################
|
88
|
+
# DO NOT EDIT, generated by Anchor Migrations
|
89
|
+
# Version: #{@migration_version}
|
90
|
+
# Source File: #{@anchor_migration_file}
|
91
|
+
# Target File: #{AnchorMigrations::RAILS_MIG_DIR}/#{@migration_version}_#{@migration_file_name_no_version}
|
92
|
+
# ################################################
|
93
|
+
#
|
94
|
+
class #{@migration_class_name} < ActiveRecord::Migration[#{rails_version_major_minor}]
|
95
|
+
disable_ddl_transaction!
|
96
|
+
|
97
|
+
def change
|
98
|
+
#{migration_change_method_body}
|
99
|
+
end
|
100
|
+
end
|
101
|
+
MIG_TEMPLATE
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/railtie" if defined?(Rails)
|
4
|
+
|
5
|
+
module AnchorMigrations
|
6
|
+
# Rails integration
|
7
|
+
class Railtie < Rails::Railtie
|
8
|
+
initializer "anchor_migrations.configure" do
|
9
|
+
AnchorMigrations.configure do |config|
|
10
|
+
config.use_strong_migrations ||= false
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnchorMigrations
|
4
|
+
# Utility functions
|
5
|
+
module Utility
|
6
|
+
# Strip out SQL comments that start with
|
7
|
+
# double hyphen "--"
|
8
|
+
def self.cleaned_sql(input_sql)
|
9
|
+
clean_lines = []
|
10
|
+
input_sql.lines.each do |line|
|
11
|
+
clean_lines << line unless line =~ /^\s*--/
|
12
|
+
end
|
13
|
+
clean_lines.join
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "anchor_migrations/configuration"
|
4
|
+
require_relative "anchor_migrations/rails_loader"
|
5
|
+
require_relative "anchor_migrations/utility"
|
6
|
+
require_relative "anchor_migrations/version"
|
7
|
+
require_relative "anchor_migrations/generator"
|
8
|
+
require_relative "anchor_migrations/rails_migration_generator"
|
9
|
+
require_relative "anchor_migrations/initializer_generator"
|
10
|
+
require_relative "anchor_migrations/cli"
|
11
|
+
require_relative "anchor_migrations/railtie" if defined?(Rails)
|
12
|
+
|
13
|
+
module AnchorMigrations
|
14
|
+
class Error < StandardError; end
|
15
|
+
|
16
|
+
class << self
|
17
|
+
attr_writer :configuration
|
18
|
+
|
19
|
+
def configuration
|
20
|
+
@configuration ||= Configuration.new
|
21
|
+
end
|
22
|
+
|
23
|
+
def configure
|
24
|
+
yield(configuration)
|
25
|
+
end
|
26
|
+
|
27
|
+
def reset
|
28
|
+
@configuration = Configuration.new
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
metadata
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: anchor_migrations
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.5
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Andrew Atkinson
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-07-12 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Description
|
14
|
+
email:
|
15
|
+
- andyatkinson@gmail.com
|
16
|
+
executables:
|
17
|
+
- anchor
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- ".rubocop.yml"
|
22
|
+
- CHANGELOG.md
|
23
|
+
- LICENSE.txt
|
24
|
+
- README.md
|
25
|
+
- Rakefile
|
26
|
+
- exe/anchor
|
27
|
+
- lib/anchor_migrations.rb
|
28
|
+
- lib/anchor_migrations/cli.rb
|
29
|
+
- lib/anchor_migrations/configuration.rb
|
30
|
+
- lib/anchor_migrations/generator.rb
|
31
|
+
- lib/anchor_migrations/initializer_generator.rb
|
32
|
+
- lib/anchor_migrations/rails_loader.rb
|
33
|
+
- lib/anchor_migrations/rails_migration_generator.rb
|
34
|
+
- lib/anchor_migrations/railtie.rb
|
35
|
+
- lib/anchor_migrations/utility.rb
|
36
|
+
- lib/anchor_migrations/version.rb
|
37
|
+
- sig/anchor_migrations.rbs
|
38
|
+
homepage: https://github.com/andyatkinson/anchor_migrations
|
39
|
+
licenses:
|
40
|
+
- MIT
|
41
|
+
metadata:
|
42
|
+
homepage_uri: https://github.com/andyatkinson/anchor_migrations
|
43
|
+
source_code_uri: https://github.com/andyatkinson/anchor_migrations
|
44
|
+
changelog_uri: https://github.com/andyatkinson/anchor_migrations/blob/main/CHANGELOG.md
|
45
|
+
post_install_message:
|
46
|
+
rdoc_options: []
|
47
|
+
require_paths:
|
48
|
+
- lib
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 2.7.8
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: '0'
|
59
|
+
requirements: []
|
60
|
+
rubygems_version: 3.1.6
|
61
|
+
signing_key:
|
62
|
+
specification_version: 4
|
63
|
+
summary: Anchor migrations
|
64
|
+
test_files: []
|