actual_db_schema 0.8.6 → 0.9.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9b9b0ce8fc7b1dbf1e0af8af4d06cdb13c60a9b36499b2de11b3d2dd320833f2
4
- data.tar.gz: bcb1f6e85b9fea8dcac89535fe4118edc9ced43f5ea57c3878d24d3b7b1786d2
3
+ metadata.gz: 3d7f7a42aa9ad38de5eeb7a64ca7a8a9117485a927d536f666aa2bf41be97b9f
4
+ data.tar.gz: 2236fd011c7aa72be7ffeef9d975da3bde1d6dd6844366cb458f51afa479395e
5
5
  SHA512:
6
- metadata.gz: caa882430fcf07bcdd6c60a8653b0792587c627b99a93843599e9cb986bba53d644875650e825ddeb73b3c168d5831728cebb5e5ef276d077663da9f9c2bcdf9
7
- data.tar.gz: f88c0acf54bba3ed626e380fa64a264573afc5ee2f1489ff6a3d3682ce2fd623d1b6b88917771811455a1e98e495b8252043cf6832044f86ccbb8da19d3d4f6e
6
+ metadata.gz: 3ed9ec959e789377596103e2be9214d5374551bbba0ab17a037cd47bc6481f035adf1b7a24bc2254702d8a64eead1fc503ee4d2317a55596258d7284500f5fd3
7
+ data.tar.gz: 48fc36d62e62e810d6d1fd4c8e3b578654587ac13f9ad8e0245e3e08c5cecf1a683ae3292715b366d0d5ce5b11cebcf953963aed8460664ddd7b0af9d7f1af7b
data/CHANGELOG.md CHANGED
@@ -1,8 +1,11 @@
1
- ## 0.8.6 - 2025-05-21
1
+ ## [0.9.0] - 2026-01-27
2
+ - Store migration files in the DB to avoid reliance on the filesystem, enabling CI/CD usage on platforms with ephemeral storage (e.g., Heroku, Docker).
3
+
4
+ ## [0.8.6] - 2025-05-21
2
5
  - Fix gem installtion with git hooks
3
6
  - Update README
4
7
 
5
- ## 0.8.5 - 2025-04-10
8
+ ## [0.8.5] - 2025-04-10
6
9
 
7
10
  - Fix the gem working on projects without git
8
11
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- actual_db_schema (0.8.6)
4
+ actual_db_schema (0.9.0)
5
5
  activerecord
6
6
  activesupport
7
7
  ast
@@ -96,7 +96,7 @@ GEM
96
96
  concurrent-ruby (1.2.2)
97
97
  connection_pool (2.4.1)
98
98
  crass (1.0.6)
99
- csv (3.3.4)
99
+ csv (3.3.5)
100
100
  date (3.3.3)
101
101
  debug (1.8.0)
102
102
  irb (>= 1.5.0)
@@ -146,7 +146,7 @@ GEM
146
146
  parser (3.2.2.4)
147
147
  ast (~> 2.4.1)
148
148
  racc
149
- prism (1.4.0)
149
+ prism (1.8.0)
150
150
  psych (5.1.1.1)
151
151
  stringio
152
152
  racc (1.7.3)
data/README.md CHANGED
@@ -2,31 +2,98 @@
2
2
 
3
3
  # ActualDbSchema
4
4
 
5
- Does switching between branches in your Rails app mess up the DB schema?
5
+ **Stop database headaches when switching Git branches in Rails**
6
+
7
+ Keep your database schema perfectly synchronized across Git branches, eliminate broken tests and schema conflicts, and save wasted hours on phantom migrations.
8
+
9
+ ## 🚀 What You Get
10
+
11
+ - **Zero Manual Work**: Switch branches freely - phantom migrations roll back automatically
12
+ - **No More Schema Conflicts**: Clean `schema.rb`/`structure.sql` diffs every time, no irrelevant changes
13
+ - **Error Prevention**: Eliminates `ActiveRecord::NotNullViolation` and similar errors when switching branches
14
+ - **Time Savings**: Stop hunting down which branch has the problematic migration
15
+ - **Team Productivity**: Everyone stays focused on coding, not database maintenance
16
+ - **Staging/Sandbox Sync**: Keep staging and sandbox databases aligned with your current branch code
17
+ - **Visual Management**: Web UI to view and manage migrations across all databases
18
+
19
+ <img width="3024" height="1886" alt="Visual management of Rails DB migrations with ActualDbSchema" src="https://github.com/user-attachments/assets/87cfb7b4-6380-4dad-ab18-6a0633f561b5" />
20
+
21
+ And you get all of that with **zero** changes to your workflow!
22
+
23
+ ## 🎯 The Problem This Solves
24
+
25
+ **Before ActualDbSchema:**
26
+ 1. Work on Branch A → Add migration → Run migration
27
+ 2. Switch to Branch B → Code breaks with database errors
28
+ 3. Manually find and rollback the "phantom" migration
29
+ 4. Deal with irrelevant `schema.rb` diffs
30
+ 5. Repeat this tedious process constantly
31
+
32
+ **After ActualDbSchema:**
33
+ 1. Work on any branch → Add migrations as usual
34
+ 2. Switch branches freely → Everything just works
35
+ 3. Focus on building features, not fixing database issues
36
+
37
+ ## 🌟 Complete Feature Set
38
+
39
+ ### Core Migration Management
40
+ - **Phantom Migration Detection**: Automatically identifies migrations from other branches
41
+ - **Smart Rollback**: Rolls back phantom migrations in correct dependency order
42
+ - **Irreversible Migration Handling**: Safely handles and reports irreversible migrations
43
+ - **Multi-Database Support**: Works seamlessly with multiple database configurations
44
+ - **Schema Format Agnostic**: Supports both `schema.rb` and `structure.sql`
45
+
46
+ ### Automation & Git Integration
47
+ - **Automatic Rollback on Migration**: Phantom migrations roll back when running `db:migrate`
48
+ - **Git Hook Integration**: Optional automatic rollback when switching branches
49
+ - **Zero Configuration**: Works out of the box with sensible defaults
50
+ - **Custom Migration Storage**: Configurable location for storing executed migrations
51
+
52
+ ### Web Interface & Management
53
+ - **Migration Dashboard**: Visual overview of all migrations across databases
54
+ - **Phantom Migration Browser**: Easy-to-use interface for viewing phantom migrations
55
+ - **One-Click Rollback**: Rollback individual or all phantom migrations via web UI
56
+ - **Broken Version Cleanup**: Identify and remove orphaned migration records
57
+ - **Schema Diff Viewer**: Visual diff of schema changes with migration annotations
58
+
59
+ ### Developer Tools
60
+ - **Console Migrations**: Run migration commands directly in Rails console
61
+ - **Schema Diff Analysis**: Annotated diffs showing which migrations caused changes
62
+ - **Migration Search & Filter**: Find specific migrations across all databases
63
+ - **Detailed Migration Info**: View migration status, branch, and database information
64
+
65
+ ### Team & Environment Support
66
+ - **Multi-Tenant Compatible**: Works with apartment gem and similar multi-tenant setups
67
+ - **Environment Flexibility**: Enable/disable features per environment
68
+ - **Team Synchronization**: Keeps all team members' databases in sync
69
+ - **CI/CD Friendly**: No interference with deployment pipelines
70
+
71
+ ### Manual Control Options
72
+ - **Manual Rollback Mode**: Disable automatic rollback for full manual control
73
+ - **Selective Rollback**: Choose which phantom migrations to rollback
74
+ - **Interactive Mode**: Step-by-step confirmation for each rollback operation
75
+ - **Rake Task Integration**: Full set of rake tasks for command-line management
76
+
77
+ ## ⚡ Quick Start
78
+
79
+ Add to your Gemfile:
6
80
 
7
- Keep the DB schema current across branches in your Rails project. Just install `actual_db_schema` gem and run `db:migrate` in branches as usual. It automatically rolls back the *phantom migrations* (non-relevant to the current branch). No additional steps are needed. It works with both `schema.rb` and `structure.sql`.
8
-
9
- ## Why ActualDbSchema
10
-
11
- Still not clear why it's needed? To grasp the purpose of this gem and the issue it addresses, review the problem definition outlined below.
12
-
13
- ### The problem definition
14
-
15
- Imagine you're working on **branch A**. You add a not-null column to a database table with a migration. You run the migration. Then you switch to **branch B**. The code in **branch B** isn't aware of this newly added field. When it tries to write data to the table, it fails with an error `null value provided for non-null field`. Why? The existing code is writing a null value into the column with a not-null constraint.
16
-
17
- Here's an example of this error:
18
-
19
- ActiveRecord::NotNullViolation:
20
- PG::NotNullViolation: ERROR: null value in column "log" of relation "check_results" violates not-null constraint
21
- DETAIL: Failing row contains (8, 46, success, 2022-10-16 21:47:21.07212, 2022-10-16 21:47:21.07212, null).
81
+ ```ruby
82
+ group :development do
83
+ gem "actual_db_schema"
84
+ end
85
+ ```
22
86
 
23
- Furthermore, the `db:migrate` task on **branch B** generates an irrelevant diff on the `schema.rb` file, reflecting the new column added in **branch A**.
87
+ Install and configure:
24
88
 
25
- To fix this, you need to switch back to **branch A**, find the migration that added the problematic field, and roll it back. We'll call it a *phantom migration*. It's a pain, especially if you have a lot of branches in your project because you have to remember which branch the *phantom migration* is in and then manually roll it back.
89
+ ```sh
90
+ bundle install
91
+ rails actual_db_schema:install
92
+ ```
26
93
 
27
- With `actual_db_schema` gem you don't need to care about that anymore. It saves you time by handling all this dirty work behind the scenes automatically.
94
+ That's it! Now just run `rails db:migrate` as usual - phantom migrations roll back automatically.
28
95
 
29
- ### How it solves the issue
96
+ ## 🔧 How It Works
30
97
 
31
98
  This gem stores all run migrations with their code in the `tmp/migrated` folder. Whenever you perform a schema dump, it rolls back the *phantom migrations*.
32
99
 
@@ -53,10 +120,10 @@ If you cannot commit changes to the repo or Gemfile, consider the local Gemfile
53
120
  Next, generate your ActualDbSchema initializer file by running:
54
121
 
55
122
  ```sh
56
- rake actual_db_schema:install
123
+ rails actual_db_schema:install
57
124
  ```
58
125
 
59
- This will create a `config/initializers/actual_db_schema.rb` file with all the available configuration options so you can adjust them as needed. It will also prompt you to install the post-checkout Git hook for automatic phantom migration rollback when switching branches.
126
+ This will create a `config/initializers/actual_db_schema.rb` file that lists all available configuration options, allowing you to customize them as needed. The installation process will also prompt you to install the post-checkout Git hook, which automatically rolls back phantom migrations when switching branches. If enabled, this hook will run the schema actualization rake task every time you switch branches, which can slow down branch changes. Therefore, you might not always want this automatic actualization on every switch; in that case, running `rails db:migrate` manually provides a faster, more controlled alternative.
60
127
 
61
128
  For more details on the available configuration options, see the sections below.
62
129
 
@@ -72,7 +139,7 @@ The gem offers the following rake tasks that can be manually run according to yo
72
139
  - `rails db:rollback_branches:manual` - run it to manually rolls back phantom migrations one by one.
73
140
  - `rails db:phantom_migrations` - displays a list of phantom migrations.
74
141
 
75
- ## Migrated Folder Configuration
142
+ ## 🎛️ Configuration Options
76
143
 
77
144
  By default, `actual_db_schema` stores all run migrations in the `tmp/migrated` folder. However, if you want to change this location, you can configure it in two ways:
78
145
 
@@ -91,13 +158,37 @@ Add the following line to your initializer file (`config/initializers/actual_db_
91
158
  config.migrated_folder = Rails.root.join("custom", "migrated")
92
159
  ```
93
160
 
94
- ## Accessing the UI
161
+ ### 3. Store migrations in the database
95
162
 
96
- The UI for managing migrations is enabled automatically. To access the UI, simply navigate to the following URL in your web browser:
163
+ If you want to share executed migrations across environments (e.g., staging or sandboxes),
164
+ store them in the main database instead of the local filesystem:
165
+
166
+ ```ruby
167
+ config.migrations_storage = :db
168
+ ```
169
+
170
+ Or via environment variable:
171
+
172
+ ```sh
173
+ export ACTUAL_DB_SCHEMA_MIGRATIONS_STORAGE="db"
174
+ ```
175
+
176
+ If both are set, the initializer setting (`config.migrations_storage`) takes precedence.
177
+
178
+ ## 🌐 Web Interface
179
+
180
+ Access the migration management UI at:
97
181
  ```
98
182
  http://localhost:3000/rails/phantom_migrations
99
183
  ```
100
- This page displays a list of phantom migrations for each database connection and provides options to view details and rollback them.
184
+
185
+ View and manage:
186
+ - **Migration Overview**: See all executed migrations with their status, branch, and database
187
+ - **Phantom Migrations**: Identify migrations from other branches that need rollback
188
+ - **Migration Source Code**: Browse the source code of every migration ever run (including the phantom ones)
189
+ - **One-Click Actions**: Rollback or migrate individual migrations directly from the UI
190
+ - **Broken Versions**: Detect and clean up orphaned migration records safely
191
+ - **Schema Diffs**: Visual diff of schema changes annotated with their source migrations
101
192
 
102
193
  ## UI options
103
194
 
@@ -193,6 +284,8 @@ If `schema.rb` generates a diff, it can be helpful to find out which migrations
193
284
 
194
285
  By default, the task uses `db/schema.rb` and `db/migrate` as the schema and migrations paths. You can also provide custom paths as arguments.
195
286
 
287
+ Alternatively, if you use Web UI, you can see this diff at `http://localhost:3000/rails/schema`. This way is often more convenient than running the Rake task manually.
288
+
196
289
  ### Usage
197
290
 
198
291
  Run the task with default paths:
@@ -290,7 +383,7 @@ rake actual_db_schema:delete_broken_versions["20250224103352 20250224103358"]
290
383
  rake actual_db_schema:delete_broken_versions["20250224103352 20250224103358", "primary"]
291
384
  ```
292
385
 
293
- ## Development
386
+ ## 🏗️ Development
294
387
 
295
388
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
296
389
 
@@ -8,10 +8,11 @@ Gem::Specification.new do |spec|
8
8
  spec.authors = ["Andrei Kaleshka"]
9
9
  spec.email = ["ka8725@gmail.com"]
10
10
 
11
- spec.summary = "Keep your DB and schema.rb consistent in dev branches."
11
+ spec.summary = "Keep DB schema in sync across branches effortlessly."
12
12
  spec.description = <<~DESC
13
- Wipe out inconsistent DB and schema.rb when switching branches.
14
- Just install this gem and use the standard rake db:migrate command.
13
+ Keep your DB schema in sync across all branches effortlessly.
14
+ Install once, then use `rails db:migrate` normally — the gem handles phantom migration rollback automatically, eliminating schema conflicts and inconsistent database states.
15
+ Stop wasting hours on DB maintenance locally, CI, staging/sandbox, or even production.
15
16
  DESC
16
17
  spec.homepage = "https://blog.widefix.com/actual-db-schema/"
17
18
  spec.license = "MIT"
@@ -31,7 +31,13 @@
31
31
  </tr>
32
32
  <tr>
33
33
  <th>Path</th>
34
- <td><%= migration[:filename] %></td>
34
+ <td>
35
+ <%= migration[:filename] %>
36
+ <% source = migration[:source].to_s %>
37
+ <% if source.present? %>
38
+ <span class="source-badge"><%= source.upcase %></span>
39
+ <% end %>
40
+ </td>
35
41
  </tr>
36
42
  </tbody>
37
43
  </table>
@@ -31,7 +31,13 @@
31
31
  </tr>
32
32
  <tr>
33
33
  <th>Path</th>
34
- <td><%= phantom_migration[:filename] %></td>
34
+ <td>
35
+ <%= phantom_migration[:filename] %>
36
+ <% source = phantom_migration[:source].to_s %>
37
+ <% if source.present? %>
38
+ <span class="source-badge"><%= source.upcase %></span>
39
+ <% end %>
40
+ </td>
35
41
  </tr>
36
42
  </tbody>
37
43
  </table>
@@ -158,4 +158,16 @@
158
158
  .schema-diff {
159
159
  margin-left: 8px;
160
160
  }
161
+
162
+ .source-badge {
163
+ display: inline-block;
164
+ margin-left: 8px;
165
+ padding: 2px 6px;
166
+ border-radius: 10px;
167
+ font-size: 11px;
168
+ font-weight: bold;
169
+ letter-spacing: 0.3px;
170
+ background-color: #e8f1ff;
171
+ color: #1d4ed8;
172
+ }
161
173
  </style>
@@ -4,16 +4,10 @@ module ActualDbSchema
4
4
  # Manages the configuration settings for the gem.
5
5
  class Configuration
6
6
  attr_accessor :enabled, :auto_rollback_disabled, :ui_enabled, :git_hooks_enabled, :multi_tenant_schemas,
7
- :console_migrations_enabled, :migrated_folder
7
+ :console_migrations_enabled, :migrated_folder, :migrations_storage
8
8
 
9
9
  def initialize
10
- @enabled = Rails.env.development?
11
- @auto_rollback_disabled = ENV["ACTUAL_DB_SCHEMA_AUTO_ROLLBACK_DISABLED"].present?
12
- @ui_enabled = Rails.env.development? || ENV["ACTUAL_DB_SCHEMA_UI_ENABLED"].present?
13
- @git_hooks_enabled = ENV["ACTUAL_DB_SCHEMA_GIT_HOOKS_ENABLED"].present?
14
- @multi_tenant_schemas = nil
15
- @console_migrations_enabled = ENV["ACTUAL_DB_SCHEMA_CONSOLE_MIGRATIONS_ENABLED"].present?
16
- @migrated_folder = ENV["ACTUAL_DB_SCHEMA_MIGRATED_FOLDER"].present?
10
+ apply_defaults(default_settings)
17
11
  end
18
12
 
19
13
  def [](key)
@@ -22,6 +16,9 @@ module ActualDbSchema
22
16
 
23
17
  def []=(key, value)
24
18
  public_send("#{key}=", value)
19
+ return unless key.to_sym == :migrations_storage && defined?(ActualDbSchema::Store)
20
+
21
+ ActualDbSchema::Store.instance.reset_adapter
25
22
  end
26
23
 
27
24
  def fetch(key, default = nil)
@@ -31,5 +28,26 @@ module ActualDbSchema
31
28
  default
32
29
  end
33
30
  end
31
+
32
+ private
33
+
34
+ def default_settings
35
+ {
36
+ enabled: Rails.env.development?,
37
+ auto_rollback_disabled: ENV["ACTUAL_DB_SCHEMA_AUTO_ROLLBACK_DISABLED"].present?,
38
+ ui_enabled: Rails.env.development? || ENV["ACTUAL_DB_SCHEMA_UI_ENABLED"].present?,
39
+ git_hooks_enabled: ENV["ACTUAL_DB_SCHEMA_GIT_HOOKS_ENABLED"].present?,
40
+ multi_tenant_schemas: nil,
41
+ console_migrations_enabled: ENV["ACTUAL_DB_SCHEMA_CONSOLE_MIGRATIONS_ENABLED"].present?,
42
+ migrated_folder: ENV["ACTUAL_DB_SCHEMA_MIGRATED_FOLDER"].present?,
43
+ migrations_storage: ENV.fetch("ACTUAL_DB_SCHEMA_MIGRATIONS_STORAGE", "file").to_sym
44
+ }
45
+ end
46
+
47
+ def apply_defaults(settings)
48
+ settings.each do |key, value|
49
+ instance_variable_set("@#{key}", value)
50
+ end
51
+ end
34
52
  end
35
53
  end
@@ -12,5 +12,35 @@ module ActualDbSchema
12
12
  end
13
13
  end
14
14
  end
15
+
16
+ initializer "actual_db_schema.schema_dump_exclusions" do
17
+ ActiveSupport.on_load(:active_record) do
18
+ table_name = ActualDbSchema::Store::DbAdapter::TABLE_NAME
19
+
20
+ if defined?(ActiveRecord::SchemaDumper) && ActiveRecord::SchemaDumper.respond_to?(:ignore_tables)
21
+ ActiveRecord::SchemaDumper.ignore_tables |= [table_name]
22
+ end
23
+
24
+ next unless defined?(ActiveRecord::Tasks::DatabaseTasks)
25
+ next unless ActiveRecord::Tasks::DatabaseTasks.respond_to?(:structure_dump_flags)
26
+
27
+ flags = Array(ActiveRecord::Tasks::DatabaseTasks.structure_dump_flags)
28
+ adapter = ActualDbSchema.db_config[:adapter].to_s
29
+ database = ActualDbSchema.db_config[:database]
30
+ if database.nil? && ActiveRecord::Base.respond_to?(:connection_db_config)
31
+ database = ActiveRecord::Base.connection_db_config&.database
32
+ end
33
+
34
+ if adapter.match?(/postgres/i)
35
+ flag = "--exclude-table=#{table_name}*"
36
+ flags << flag unless flags.include?(flag)
37
+ elsif adapter.match?(/mysql/i) && database
38
+ flag = "--ignore-table=#{database}.#{table_name}"
39
+ flags << flag unless flags.include?(flag)
40
+ end
41
+
42
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump_flags = flags
43
+ end
44
+ end
15
45
  end
16
46
  end
@@ -5,7 +5,8 @@ module ActualDbSchema
5
5
  class Migration
6
6
  include Singleton
7
7
 
8
- Migration = Struct.new(:status, :version, :name, :branch, :database, :filename, :phantom, keyword_init: true)
8
+ Migration = Struct.new(:status, :version, :name, :branch, :database, :filename, :phantom, :source,
9
+ keyword_init: true)
9
10
 
10
11
  def all_phantom
11
12
  migrations = []
@@ -120,7 +121,8 @@ module ActualDbSchema
120
121
  branch: branch_for(migration.version),
121
122
  database: ActualDbSchema.db_config[:database],
122
123
  filename: migration.filename,
123
- phantom: phantom?(migration)
124
+ phantom: phantom?(migration),
125
+ source: ActualDbSchema::Store.instance.source_for(migration.version)
124
126
  )
125
127
  end
126
128
 
@@ -129,8 +131,7 @@ module ActualDbSchema
129
131
  end
130
132
 
131
133
  def phantom?(migration)
132
- migrated_folder = ActualDbSchema.config[:migrated_folder].presence || "/tmp/migrated"
133
- migration.filename.include?(migrated_folder.to_s)
134
+ ActualDbSchema::Store.instance.stored_migration?(migration.filename)
134
135
  end
135
136
 
136
137
  def should_include?(status, migration)
@@ -71,7 +71,7 @@ module ActualDbSchema
71
71
  def migration_files
72
72
  paths = Array(migrations_paths)
73
73
  current_branch_files = Dir[*paths.flat_map { |path| "#{path}/**/[0-9]*_*.rb" }]
74
- other_branches_files = Dir["#{ActualDbSchema.migrated_folder}/**/[0-9]*_*.rb"]
74
+ other_branches_files = ActualDbSchema::Store.instance.migration_files
75
75
  current_branch_versions = current_branch_files.map { |file| file.match(/(\d+)_/)[1] }
76
76
  filtered_other_branches_files = other_branches_files.reject do |file|
77
77
  version = file.match(/(\d+)_/)[1]
@@ -163,7 +163,7 @@ module ActualDbSchema
163
163
 
164
164
  migrations.uniq.each do |migration|
165
165
  count = migration_counts[migration.filename]
166
- File.delete(migration.filename) if count == schema_count && File.exist?(migration.filename)
166
+ ActualDbSchema::Store.instance.delete(migration.filename) if count == schema_count
167
167
  end
168
168
  end
169
169
 
@@ -63,12 +63,12 @@ module ActualDbSchema
63
63
  end
64
64
 
65
65
  def migrated_folders
66
+ ActualDbSchema::Store.instance.materialize_all
66
67
  dirs = find_migrated_folders
67
68
 
68
- if (configured_migrated_folder = ActualDbSchema.config[:migrated_folder].presence)
69
- relative_migrated_folder = configured_migrated_folder.to_s.sub(%r{\A#{Regexp.escape(Rails.root.to_s)}/?}, "")
70
- dirs << relative_migrated_folder unless dirs.include?(relative_migrated_folder)
71
- end
69
+ configured_migrated_folder = ActualDbSchema.migrated_folder
70
+ relative_migrated_folder = configured_migrated_folder.to_s.sub(%r{\A#{Regexp.escape(Rails.root.to_s)}/?}, "")
71
+ dirs << relative_migrated_folder unless dirs.include?(relative_migrated_folder)
72
72
 
73
73
  dirs.map { |dir| dir.sub(%r{\A\./}, "") }.uniq
74
74
  end
@@ -1,44 +1,305 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActualDbSchema
4
- # Stores the migrated files into the tmp folder
4
+ # Stores migration sources and metadata.
5
5
  class Store
6
6
  include Singleton
7
7
 
8
8
  Item = Struct.new(:version, :timestamp, :branch)
9
9
 
10
10
  def write(filename)
11
- basename = File.basename(filename)
12
- FileUtils.mkdir_p(folder)
13
- FileUtils.copy(filename, folder.join(basename))
14
- record_metadata(filename)
11
+ adapter.write(filename)
12
+ reset_source_cache
15
13
  end
16
14
 
17
15
  def read
18
- return {} unless File.exist?(store_file)
16
+ adapter.read
17
+ end
18
+
19
+ def migration_files
20
+ adapter.migration_files
21
+ end
22
+
23
+ def delete(filename)
24
+ adapter.delete(filename)
25
+ reset_source_cache
26
+ end
27
+
28
+ def stored_migration?(filename)
29
+ adapter.stored_migration?(filename)
30
+ end
31
+
32
+ def source_for(version)
33
+ version = version.to_s
34
+
35
+ return :db if db_versions.key?(version)
36
+ return :file if file_versions.key?(version)
37
+
38
+ :unknown
39
+ end
40
+
41
+ def materialize_all
42
+ adapter.materialize_all
43
+ end
19
44
 
20
- CSV.read(store_file).map { |line| Item.new(*line) }.index_by(&:version)
45
+ def reset_adapter
46
+ @adapter = nil
47
+ reset_source_cache
21
48
  end
22
49
 
23
50
  private
24
51
 
25
- def record_metadata(filename)
26
- version = File.basename(filename).scan(/(\d+)_.*\.rb/).first.first
27
- CSV.open(store_file, "a") do |csv|
28
- csv << [
29
- version,
30
- Time.current.iso8601,
31
- Git.current_branch
32
- ]
52
+ def adapter
53
+ @adapter ||= begin
54
+ storage = ActualDbSchema.config[:migrations_storage].to_s
55
+ storage == "db" ? DbAdapter.new : FileAdapter.new
56
+ end
57
+ end
58
+
59
+ def reset_source_cache
60
+ @db_versions = nil
61
+ @file_versions = nil
62
+ end
63
+
64
+ def db_versions
65
+ @db_versions ||= begin
66
+ connection = ActiveRecord::Base.connection
67
+ return {} unless connection.table_exists?(DbAdapter::TABLE_NAME)
68
+
69
+ table = connection.quote_table_name(DbAdapter::TABLE_NAME)
70
+ connection.select_values("SELECT version FROM #{table}").each_with_object({}) do |version, acc|
71
+ acc[version.to_s] = true
72
+ end
73
+ rescue StandardError
74
+ {}
33
75
  end
34
76
  end
35
77
 
36
- def folder
37
- ActualDbSchema.migrated_folder
78
+ def file_versions
79
+ @file_versions ||= FileAdapter.new.read
80
+ rescue StandardError
81
+ {}
38
82
  end
39
83
 
40
- def store_file
41
- folder.join("metadata.csv")
84
+ # Stores migrated files on the filesystem with metadata in CSV.
85
+ class FileAdapter
86
+ def write(filename)
87
+ basename = File.basename(filename)
88
+ FileUtils.mkdir_p(folder)
89
+ FileUtils.copy(filename, folder.join(basename))
90
+ record_metadata(filename)
91
+ end
92
+
93
+ def read
94
+ return {} unless File.exist?(store_file)
95
+
96
+ CSV.read(store_file).map { |line| Item.new(*line) }.index_by(&:version)
97
+ end
98
+
99
+ def migration_files
100
+ Dir["#{folder}/**/[0-9]*_*.rb"]
101
+ end
102
+
103
+ def delete(filename)
104
+ File.delete(filename) if File.exist?(filename)
105
+ end
106
+
107
+ def stored_migration?(filename)
108
+ filename.to_s.start_with?(folder.to_s)
109
+ end
110
+
111
+ def materialize_all
112
+ nil
113
+ end
114
+
115
+ private
116
+
117
+ def record_metadata(filename)
118
+ version = File.basename(filename).scan(/(\d+)_.*\.rb/).first.first
119
+ CSV.open(store_file, "a") do |csv|
120
+ csv << [
121
+ version,
122
+ Time.current.iso8601,
123
+ Git.current_branch
124
+ ]
125
+ end
126
+ end
127
+
128
+ def folder
129
+ ActualDbSchema.migrated_folder
130
+ end
131
+
132
+ def store_file
133
+ folder.join("metadata.csv")
134
+ end
135
+ end
136
+
137
+ # Stores migrated files in the database.
138
+ class DbAdapter
139
+ TABLE_NAME = "actual_db_schema_migrations"
140
+ RECORD_COLUMNS = %w[version filename content branch migrated_at].freeze
141
+
142
+ def write(filename)
143
+ ensure_table!
144
+
145
+ version = extract_version(filename)
146
+ return unless version
147
+
148
+ basename = File.basename(filename)
149
+ content = File.read(filename)
150
+ upsert_record(version, basename, content, Git.current_branch, Time.current)
151
+ write_cache_file(basename, content)
152
+ end
153
+
154
+ def read
155
+ return {} unless table_exists?
156
+
157
+ rows = connection.exec_query(<<~SQL.squish)
158
+ SELECT version, migrated_at, branch
159
+ FROM #{quoted_table}
160
+ SQL
161
+
162
+ rows.map do |row|
163
+ Item.new(row["version"].to_s, row["migrated_at"], row["branch"])
164
+ end.index_by(&:version)
165
+ end
166
+
167
+ def migration_files
168
+ materialize_all
169
+ Dir["#{folder}/**/[0-9]*_*.rb"]
170
+ end
171
+
172
+ def delete(filename)
173
+ version = extract_version(filename)
174
+ return unless version
175
+
176
+ if table_exists?
177
+ connection.execute(<<~SQL.squish)
178
+ DELETE FROM #{quoted_table}
179
+ WHERE #{quoted_column("version")} = #{connection.quote(version)}
180
+ SQL
181
+ end
182
+ File.delete(filename) if File.exist?(filename)
183
+ end
184
+
185
+ def stored_migration?(filename)
186
+ filename.to_s.start_with?(folder.to_s)
187
+ end
188
+
189
+ def materialize_all
190
+ return unless table_exists?
191
+
192
+ FileUtils.mkdir_p(folder)
193
+ rows = connection.exec_query(<<~SQL.squish)
194
+ SELECT filename, content
195
+ FROM #{quoted_table}
196
+ SQL
197
+
198
+ rows.each do |row|
199
+ write_cache_file(row["filename"], row["content"])
200
+ end
201
+ end
202
+
203
+ private
204
+
205
+ def upsert_record(version, basename, content, branch, migrated_at)
206
+ attributes = record_attributes(version, basename, content, branch, migrated_at)
207
+ record_exists?(version) ? update_record(attributes) : insert_record(attributes)
208
+ end
209
+
210
+ def record_attributes(version, basename, content, branch, migrated_at)
211
+ {
212
+ version: version,
213
+ filename: basename,
214
+ content: content,
215
+ branch: branch,
216
+ migrated_at: migrated_at
217
+ }
218
+ end
219
+
220
+ def update_record(attributes)
221
+ assignments = record_columns.reject { |column| column == "version" }.map do |column|
222
+ "#{quoted_column(column)} = #{connection.quote(attributes[column.to_sym])}"
223
+ end
224
+
225
+ connection.execute(<<~SQL)
226
+ UPDATE #{quoted_table}
227
+ SET #{assignments.join(", ")}
228
+ WHERE #{quoted_column("version")} = #{connection.quote(attributes[:version])}
229
+ SQL
230
+ end
231
+
232
+ def insert_record(attributes)
233
+ columns = record_columns
234
+ values = columns.map { |column| connection.quote(attributes[column.to_sym]) }
235
+
236
+ connection.execute(<<~SQL)
237
+ INSERT INTO #{quoted_table}
238
+ (#{columns.map { |column| quoted_column(column) }.join(", ")})
239
+ VALUES
240
+ (#{values.join(", ")})
241
+ SQL
242
+ end
243
+
244
+ def record_exists?(version)
245
+ connection.select_value(<<~SQL.squish).present?
246
+ SELECT 1
247
+ FROM #{quoted_table}
248
+ WHERE #{quoted_column("version")} = #{connection.quote(version)}
249
+ LIMIT 1
250
+ SQL
251
+ end
252
+
253
+ def ensure_table!
254
+ return if table_exists?
255
+
256
+ connection.create_table(TABLE_NAME) do |t|
257
+ t.string :version, null: false
258
+ t.string :filename, null: false
259
+ t.text :content, null: false
260
+ t.string :branch
261
+ t.datetime :migrated_at, null: false
262
+ end
263
+
264
+ connection.add_index(TABLE_NAME, :version, unique: true) unless connection.index_exists?(TABLE_NAME, :version)
265
+ end
266
+
267
+ def table_exists?
268
+ connection.table_exists?(TABLE_NAME)
269
+ end
270
+
271
+ def connection
272
+ ActiveRecord::Base.connection
273
+ end
274
+
275
+ def record_columns
276
+ RECORD_COLUMNS
277
+ end
278
+
279
+ def quoted_table
280
+ connection.quote_table_name(TABLE_NAME)
281
+ end
282
+
283
+ def quoted_column(name)
284
+ connection.quote_column_name(name)
285
+ end
286
+
287
+ def folder
288
+ ActualDbSchema.migrated_folder
289
+ end
290
+
291
+ def write_cache_file(filename, content)
292
+ FileUtils.mkdir_p(folder)
293
+ path = folder.join(File.basename(filename))
294
+ return if File.exist?(path) && File.read(path) == content
295
+
296
+ File.write(path, content)
297
+ end
298
+
299
+ def extract_version(filename)
300
+ match = File.basename(filename).scan(/(\d+)_.*\.rb/).first
301
+ match&.first
302
+ end
42
303
  end
43
304
  end
44
305
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActualDbSchema
4
- VERSION = "0.8.6"
4
+ VERSION = "0.9.0"
5
5
  end
@@ -30,5 +30,9 @@ if defined?(ActualDbSchema)
30
30
  # Define the migrated folder location.
31
31
  # config.migrated_folder = Rails.root.join("custom", "migrated")
32
32
  config.migrated_folder = Rails.root.join("tmp", "migrated")
33
+
34
+ # Choose where to store migrated files: :file or :db.
35
+ # config.migrations_storage = :db
36
+ config.migrations_storage = :file
33
37
  end
34
38
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: actual_db_schema
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.6
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Kaleshka
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-05-21 00:00:00.000000000 Z
11
+ date: 2026-01-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -151,8 +151,9 @@ dependencies:
151
151
  - !ruby/object:Gem::Version
152
152
  version: '0'
153
153
  description: |
154
- Wipe out inconsistent DB and schema.rb when switching branches.
155
- Just install this gem and use the standard rake db:migrate command.
154
+ Keep your DB schema in sync across all branches effortlessly.
155
+ Install once, then use `rails db:migrate` normally — the gem handles phantom migration rollback automatically, eliminating schema conflicts and inconsistent database states.
156
+ Stop wasting hours on DB maintenance locally, CI, staging/sandbox, or even production.
156
157
  email:
157
158
  - ka8725@gmail.com
158
159
  executables: []
@@ -253,6 +254,6 @@ requirements: []
253
254
  rubygems_version: 3.3.26
254
255
  signing_key:
255
256
  specification_version: 4
256
- summary: Keep your DB and schema.rb consistent in dev branches.
257
+ summary: Keep DB schema in sync across branches effortlessly.
257
258
  test_files: []
258
259
  ...