actual_db_schema 0.9.1 → 0.10.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 +4 -4
- data/CHANGELOG.md +11 -0
- data/Gemfile.lock +2 -4
- data/README.md +76 -31
- data/actual_db_schema.gemspec +5 -7
- data/lib/actual_db_schema/engine.rb +7 -0
- data/lib/actual_db_schema/middlewares/skip_pending_migration_check.rb +39 -0
- data/lib/actual_db_schema/migration_context.rb +11 -0
- data/lib/actual_db_schema/migration_parser.rb +183 -184
- data/lib/actual_db_schema/patches/check_pending.rb +15 -0
- data/lib/actual_db_schema/schema_parser.rb +77 -98
- data/lib/actual_db_schema/version.rb +1 -1
- data/lib/actual_db_schema.rb +2 -0
- metadata +7 -34
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f52b65d1b46f79d260c1e2d8c6fa768e03d991220954a88d7ff9f3c9f9e31a7b
|
|
4
|
+
data.tar.gz: c50d05e0800fd85c35cc84f570342e67af2c5212c1629cb93facfcde2dd6b749
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3d4af8729461e9c106215a72d78d22628217e1c1138307a39056fcd02e567f656ca6452b5eee20a82e886b141c11c35ce8eff51785758661cc3daa59d8fef259
|
|
7
|
+
data.tar.gz: 5ca66e12fdb0969b1a8296b945231286908069e789d6b7329fd84f3459a4d2c2d502621676cf220fade2bc628f1c5cf8c35cae8a8a101d5e99b2f711a63a0764
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
## [0.10.0] - 2026-10-11
|
|
2
|
+
|
|
3
|
+
- Remove the `parser` dependency and use `prism` directly.
|
|
4
|
+
- Update the README and gem description to explain database synchronization, migration management, and schema diff troubleshooting.
|
|
5
|
+
- Add an animated demo to the README; exclude demo media from the gem package.
|
|
6
|
+
|
|
7
|
+
## [0.9.2] - 2026-06-14
|
|
8
|
+
|
|
9
|
+
- Skip pending migrations in the UI
|
|
10
|
+
- Restore original connection after iterating through connections
|
|
11
|
+
|
|
1
12
|
## [0.9.1] - 2026-02-25
|
|
2
13
|
|
|
3
14
|
- Support schema diffs for `structure.sql`
|
data/Gemfile.lock
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
actual_db_schema (0.
|
|
4
|
+
actual_db_schema (0.10.0)
|
|
5
5
|
activerecord
|
|
6
6
|
activesupport
|
|
7
|
-
ast
|
|
8
7
|
csv
|
|
9
|
-
parser
|
|
10
8
|
prism
|
|
11
9
|
|
|
12
10
|
GEM
|
|
@@ -96,7 +94,7 @@ GEM
|
|
|
96
94
|
concurrent-ruby (1.2.2)
|
|
97
95
|
connection_pool (2.4.1)
|
|
98
96
|
crass (1.0.6)
|
|
99
|
-
csv (3.3.
|
|
97
|
+
csv (3.3.6)
|
|
100
98
|
date (3.3.3)
|
|
101
99
|
debug (1.8.0)
|
|
102
100
|
irb (>= 1.5.0)
|
data/README.md
CHANGED
|
@@ -1,46 +1,91 @@
|
|
|
1
1
|
[](https://badge.fury.io/rb/actual_db_schema)
|
|
2
2
|
|
|
3
|
-
# ActualDbSchema
|
|
3
|
+
# ActualDbSchema: Keep Rails Databases in Sync with Git Branches
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**Manage stale migrations, fix database drift, and understand schema changes in Rails.**
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
`actual_db_schema` keeps your database aligned with the migrations on your current Git branch. It tracks executed migration source code and rolls back **phantom migrations** left behind by other branches. It also provides a web dashboard, annotated schema diffs, migration history, and manual cleanup tools for development, staging, and sandbox environments.
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## Problems ActualDbSchema Solves
|
|
10
10
|
|
|
11
|
-
|
|
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
|
|
11
|
+
### Database errors and failing tests after switching branches
|
|
18
12
|
|
|
19
|
-
|
|
13
|
+
Switching Git branches changes your code and migration files, but does not undo migrations already applied to your database. A column, table, index, or constraint from another branch can break the application or its tests. For example, an extra `NOT NULL` column can cause `ActiveRecord::NotNullViolation` when the current branch inserts a record.
|
|
20
14
|
|
|
21
|
-
|
|
15
|
+
ActualDbSchema rolls back tracked migrations missing from the current branch, removing their database changes when their reversals succeed.
|
|
22
16
|
|
|
23
|
-
|
|
17
|
+
### Manual rollback work when moving between feature branches
|
|
24
18
|
|
|
25
|
-
**
|
|
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
|
|
19
|
+
Without a stored copy of a migration, you may need to find the branch that introduced it, check it out, roll it back, and return to your work. These stale migrations are called **phantom migrations**: they have been applied to the database, but their files are absent from the current branch.
|
|
31
20
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
21
|
+
The gem saves migration source code and branch metadata so it can identify and roll back phantom migrations without checking out their original branch. Cleanup runs as part of the schema dump triggered by `rails db:migrate`. An optional [post-checkout Git hook](#automatic-phantom-migration-rollback-on-branch-switch) runs cleanup when you switch branches. You can also inspect and roll back migrations manually through rake tasks or the web UI.
|
|
22
|
+
|
|
23
|
+
### Unwanted schema.rb diffs and schema conflicts in Git
|
|
24
|
+
|
|
25
|
+
**Why does `schema.rb` keep changing after `db:migrate`, even when you have not changed a migration?** Rails dumps your database's actual structure, including leftover tables, columns, and indexes from other branches. Those changes pollute `db/schema.rb` or `db/structure.sql` and can lead to unrelated pull request changes and Git merge conflicts.
|
|
26
|
+
|
|
27
|
+
ActualDbSchema rolls back tracked phantom migrations before the schema dump, preventing their leftover changes from appearing in the generated file. Legitimate schema changes from the current branch still appear in the diff. Both schema formats are supported.
|
|
28
|
+
|
|
29
|
+
### Rebuilding the entire database to clean up stale migrations
|
|
30
|
+
|
|
31
|
+
Resetting a development database and replaying migrations can take time and discard local data. ActualDbSchema targets tracked phantom migrations instead of requiring a full database reset. Each rollback still executes the migration's reversal and can remove data in affected tables or columns.
|
|
32
|
+
|
|
33
|
+
### Difficulty tracing a schema change to its migration
|
|
34
|
+
|
|
35
|
+
When a schema diff contains unexpected changes, it can be hard to tell which migration introduced them. The [annotated schema diff](#schema-diff-with-migration-annotations) associates changes with migration source files, available through a rake task or the web UI. This helps you investigate a diff before deciding how to resolve it.
|
|
36
|
+
|
|
37
|
+
### Missing visibility into migration history and database state
|
|
38
|
+
|
|
39
|
+
The [web dashboard](#-web-interface) lets you inspect migration status, source code, branch metadata, and phantom migrations across configured databases. It supports individual migration actions and remains accessible when pending migrations would otherwise block Rails requests.
|
|
40
|
+
|
|
41
|
+
### Orphaned migration versions with missing source files
|
|
42
|
+
|
|
43
|
+
Your database can contain migration version records whose source files are no longer available. The [broken migration cleanup tools](#delete-broken-migrations) identify and delete these orphaned records through the UI or a rake task. Deleting a version record does **not** reverse its database changes; use phantom migration rollback when stored source code is available.
|
|
44
|
+
|
|
45
|
+
### Database drift across multiple databases, tenants, and shared environments
|
|
46
|
+
|
|
47
|
+
Branch-specific migrations can leave several databases or tenant schemas out of sync, not just the primary development database. ActualDbSchema processes configured databases, supports a configurable list of [tenant schemas](#multi-tenancy-support), and lets you [exclude infrastructure databases](#excluding-databases-from-processing).
|
|
48
|
+
|
|
49
|
+
For staging or sandbox environments, optional [database-backed migration storage](#3-store-migrations-in-the-database) retains executed migration sources in the database rather than relying on one machine's local files. Features can be enabled per environment.
|
|
50
|
+
|
|
51
|
+
### Friction when experimenting with schema changes in the Rails console
|
|
52
|
+
|
|
53
|
+
The optional [console migrations](#console-migrations) feature exposes migration methods such as `add_column`, `add_index`, and `create_table` directly in the Rails console. This helps with local schema experiments and manual repairs; it does not create a migration file for you.
|
|
54
|
+
|
|
55
|
+
## Example: Switch Branches Without Leaving Phantom Migrations Behind
|
|
56
|
+
|
|
57
|
+
Suppose a feature branch adds a required `users.nickname` column. After switching to `main`, which has no such migration, inserts may fail because the application does not populate that column. The next schema dump can also add the unrelated column to `schema.rb`.
|
|
58
|
+
|
|
59
|
+
With `actual_db_schema` installed ([Quick Start](#-quick-start)), keep using your normal workflow:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
git switch main
|
|
63
|
+
bundle exec rails db:migrate
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The gem uses its stored migration source to roll back the phantom migration before Rails dumps the schema. When the rollback succeeds, the extra column is removed, fixing both the stale constraint and its unwanted schema diff.
|
|
67
|
+
|
|
68
|
+
## Scope and Rollback Requirements
|
|
69
|
+
|
|
70
|
+
The gem needs a stored copy of a migration to roll it back, so install it before running migrations you want it to track. Irreversible migrations and rollbacks that depend on unavailable branch code require manual intervention; failed rollbacks are reported. Phantom migrations are rolled back in reverse migration order.
|
|
71
|
+
|
|
72
|
+
ActualDbSchema does not detect edits to an already-applied migration that remains on the current branch. Its schema cleanup targets phantom migrations; it does not sort schema columns or resolve every Git merge conflict.
|
|
73
|
+
|
|
74
|
+
## Animated Demo
|
|
75
|
+
|
|
76
|
+
[](docs/actual_db_schema_h264.mp4)
|
|
77
|
+
|
|
78
|
+
[Watch the full-resolution video](docs/actual_db_schema_h264.mp4)
|
|
79
|
+
|
|
80
|
+
<img width="3024" height="1886" alt="ActualDbSchema Rails migration dashboard showing migration history and database management" src="https://github.com/user-attachments/assets/87cfb7b4-6380-4dad-ab18-6a0633f561b5" />
|
|
36
81
|
|
|
37
82
|
## 🌟 Complete Feature Set
|
|
38
83
|
|
|
39
84
|
### Core Migration Management
|
|
40
85
|
- **Phantom Migration Detection**: Automatically identifies migrations from other branches
|
|
41
|
-
- **Smart Rollback**: Rolls back phantom migrations in
|
|
42
|
-
- **
|
|
43
|
-
- **Multi-Database Support**:
|
|
86
|
+
- **Smart Rollback**: Rolls back phantom migrations in reverse migration order
|
|
87
|
+
- **Rollback Failure Reporting**: Reports irreversible migrations and other rollback failures for manual resolution
|
|
88
|
+
- **Multi-Database Support**: Processes multiple database configurations with configurable exclusions
|
|
44
89
|
- **Schema Format Agnostic**: Supports both `schema.rb` and `structure.sql`
|
|
45
90
|
|
|
46
91
|
### Automation & Git Integration
|
|
@@ -65,8 +110,8 @@ And you get all of that with **zero** changes to your workflow!
|
|
|
65
110
|
### Team & Environment Support
|
|
66
111
|
- **Multi-Tenant Compatible**: Works with apartment gem and similar multi-tenant setups
|
|
67
112
|
- **Environment Flexibility**: Enable/disable features per environment
|
|
68
|
-
- **
|
|
69
|
-
- **
|
|
113
|
+
- **Shared Migration Storage**: Optional database storage retains migration sources across environments
|
|
114
|
+
- **Rollback Instrumentation**: Emits notifications for successful rollbacks and provides optional persisted statistics
|
|
70
115
|
|
|
71
116
|
### Manual Control Options
|
|
72
117
|
- **Manual Rollback Mode**: Disable automatic rollback for full manual control
|
|
@@ -95,11 +140,11 @@ That's it! Now just run `rails db:migrate` as usual - phantom migrations roll ba
|
|
|
95
140
|
|
|
96
141
|
## 🔧 How It Works
|
|
97
142
|
|
|
98
|
-
This gem stores
|
|
143
|
+
This gem stores migrations executed while it is installed, including their source code, in the `tmp/migrated` folder by default. [Database storage](#3-store-migrations-in-the-database) is also available. Before the Rails `db:schema:dump` task runs, it attempts to roll back the *phantom migrations*.
|
|
99
144
|
|
|
100
145
|
The *phantom migrations* list is the difference between the migrations you've executed (in the `tmp/migrated` folder) and the current ones (in the `db/migrate` folder).
|
|
101
146
|
|
|
102
|
-
|
|
147
|
+
Run `rails db:migrate` in your current branch as usual. With automatic rollback enabled (the default), `actual_db_schema` rolls back tracked phantom migrations in reverse migration order before the schema dump, removing their leftover schema changes when the rollbacks succeed.
|
|
103
148
|
|
|
104
149
|
## Installation
|
|
105
150
|
|
data/actual_db_schema.gemspec
CHANGED
|
@@ -8,11 +8,10 @@ Gem::Specification.new do |spec|
|
|
|
8
8
|
spec.authors = ["Andrei Kaleshka"]
|
|
9
9
|
spec.email = ["ka8725@gmail.com"]
|
|
10
10
|
|
|
11
|
-
spec.summary = "Keep
|
|
11
|
+
spec.summary = "Keep Rails databases in sync across Git branches."
|
|
12
12
|
spec.description = <<~DESC
|
|
13
|
-
Keep
|
|
14
|
-
|
|
15
|
-
Stop wasting hours on DB maintenance locally, CI, staging/sandbox, or even production.
|
|
13
|
+
Keep Rails databases in sync across Git branches. Automatically roll back stale migrations,
|
|
14
|
+
prevent database errors and unwanted schema diffs, and inspect migration history with a web dashboard.
|
|
16
15
|
DESC
|
|
17
16
|
spec.homepage = "https://blog.widefix.com/actual-db-schema/"
|
|
18
17
|
spec.license = "MIT"
|
|
@@ -28,7 +27,8 @@ Gem::Specification.new do |spec|
|
|
|
28
27
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
29
28
|
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
30
29
|
`git ls-files -z`.split("\x0").reject do |f|
|
|
31
|
-
(f == __FILE__) ||
|
|
30
|
+
(f == __FILE__) || %w[docs/actual_db_schema_h264.mp4 docs/actual_db_schema_demo.gif].include?(f) ||
|
|
31
|
+
f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
|
|
32
32
|
end
|
|
33
33
|
end
|
|
34
34
|
spec.bindir = "exe"
|
|
@@ -38,9 +38,7 @@ Gem::Specification.new do |spec|
|
|
|
38
38
|
# Uncomment to register a new dependency of your gem
|
|
39
39
|
spec.add_runtime_dependency "activerecord"
|
|
40
40
|
spec.add_runtime_dependency "activesupport"
|
|
41
|
-
spec.add_runtime_dependency "ast"
|
|
42
41
|
spec.add_runtime_dependency "csv"
|
|
43
|
-
spec.add_runtime_dependency "parser"
|
|
44
42
|
spec.add_runtime_dependency "prism"
|
|
45
43
|
|
|
46
44
|
spec.add_development_dependency "appraisal"
|
|
@@ -10,6 +10,13 @@ module ActualDbSchema
|
|
|
10
10
|
app.routes.append do
|
|
11
11
|
mount ActualDbSchema::Engine => "/rails"
|
|
12
12
|
end
|
|
13
|
+
|
|
14
|
+
ActiveRecord::Migration::CheckPending.prepend(ActualDbSchema::Patches::CheckPending)
|
|
15
|
+
|
|
16
|
+
app.middleware.insert_before(
|
|
17
|
+
ActiveRecord::Migration::CheckPending,
|
|
18
|
+
ActualDbSchema::Middlewares::SkipPendingMigrationCheck
|
|
19
|
+
)
|
|
13
20
|
end
|
|
14
21
|
end
|
|
15
22
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActualDbSchema
|
|
4
|
+
module Middlewares
|
|
5
|
+
# Middleware that skips the pending migration check for ActualDbSchema UI routes.
|
|
6
|
+
# This allows accessing /rails/ paths even when there are pending migrations.
|
|
7
|
+
class SkipPendingMigrationCheck
|
|
8
|
+
def initialize(app)
|
|
9
|
+
@app = app
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call(env)
|
|
13
|
+
if actual_db_schema_path?(env["PATH_INFO"], env["REQUEST_METHOD"])
|
|
14
|
+
env["actual_db_schema.skip_pending_check"] = true
|
|
15
|
+
end
|
|
16
|
+
@app.call(env)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def actual_db_schema_path?(path, method)
|
|
22
|
+
return false unless path.start_with?(engine_mount_path)
|
|
23
|
+
|
|
24
|
+
ActualDbSchema::Engine.routes.recognize_path(
|
|
25
|
+
path.sub(engine_mount_path, "") || "/",
|
|
26
|
+
method: method
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
true
|
|
30
|
+
rescue ActionController::RoutingError
|
|
31
|
+
false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def engine_mount_path
|
|
35
|
+
@engine_mount_path ||= ActualDbSchema::Engine.routes.find_script_name({})
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -6,10 +6,13 @@ module ActualDbSchema
|
|
|
6
6
|
include Singleton
|
|
7
7
|
|
|
8
8
|
def each
|
|
9
|
+
original_config = current_config
|
|
9
10
|
configs.each do |db_config|
|
|
10
11
|
establish_connection(db_config)
|
|
11
12
|
yield context
|
|
12
13
|
end
|
|
14
|
+
ensure
|
|
15
|
+
establish_connection(original_config) if original_config
|
|
13
16
|
end
|
|
14
17
|
|
|
15
18
|
private
|
|
@@ -19,6 +22,14 @@ module ActualDbSchema
|
|
|
19
22
|
ActiveRecord::Base.establish_connection(config)
|
|
20
23
|
end
|
|
21
24
|
|
|
25
|
+
def current_config
|
|
26
|
+
if ActiveRecord::Base.respond_to?(:connection_db_config)
|
|
27
|
+
ActiveRecord::Base.connection_db_config
|
|
28
|
+
else
|
|
29
|
+
ActiveRecord::Base.connection_config
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
22
33
|
def configs
|
|
23
34
|
all_configs = if ActiveRecord::Base.configurations.is_a?(Hash)
|
|
24
35
|
# Rails < 6.0 has a Hash in configurations
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "ast"
|
|
4
3
|
require "prism"
|
|
5
4
|
|
|
6
5
|
module ActualDbSchema
|
|
@@ -8,18 +7,6 @@ module ActualDbSchema
|
|
|
8
7
|
module MigrationParser
|
|
9
8
|
extend self
|
|
10
9
|
|
|
11
|
-
PARSER_MAPPING = {
|
|
12
|
-
add_column: ->(args) { parse_add_column(args) },
|
|
13
|
-
change_column: ->(args) { parse_change_column(args) },
|
|
14
|
-
remove_column: ->(args) { parse_remove_column(args) },
|
|
15
|
-
rename_column: ->(args) { parse_rename_column(args) },
|
|
16
|
-
add_index: ->(args) { parse_add_index(args) },
|
|
17
|
-
remove_index: ->(args) { parse_remove_index(args) },
|
|
18
|
-
rename_index: ->(args) { parse_rename_index(args) },
|
|
19
|
-
create_table: ->(args) { parse_create_table(args) },
|
|
20
|
-
drop_table: ->(args) { parse_drop_table(args) }
|
|
21
|
-
}.freeze
|
|
22
|
-
|
|
23
10
|
def parse_all_migrations(dirs)
|
|
24
11
|
changes_by_path = {}
|
|
25
12
|
handled_files = Set.new
|
|
@@ -41,221 +28,233 @@ module ActualDbSchema
|
|
|
41
28
|
private
|
|
42
29
|
|
|
43
30
|
def parse_file(file_path)
|
|
44
|
-
Prism
|
|
31
|
+
parse_result = Prism.parse_file(file_path)
|
|
32
|
+
raise SyntaxError, "Migration #{file_path} is syntax invalid" unless parse_result.success?
|
|
33
|
+
|
|
34
|
+
parse_result.value
|
|
45
35
|
end
|
|
46
36
|
|
|
47
37
|
def find_migration_changes(node)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
changes
|
|
51
|
-
if node.type == :block
|
|
52
|
-
return process_block_node(node)
|
|
53
|
-
elsif node.type == :send
|
|
54
|
-
changes.concat(process_send_node(node))
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
node.children.each { |child| changes.concat(find_migration_changes(child)) if child.is_a?(Parser::AST::Node) }
|
|
58
|
-
|
|
59
|
-
changes
|
|
38
|
+
visitor = MigrationVisitor.new
|
|
39
|
+
visitor.visit(node)
|
|
40
|
+
visitor.changes
|
|
60
41
|
end
|
|
61
42
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
43
|
+
# Internal class used to process the AST and collect migration information.
|
|
44
|
+
class MigrationVisitor < Prism::Visitor
|
|
45
|
+
MAPPING = {
|
|
46
|
+
add_column: ->(visitor, args) { visitor.parse_add_column(args) },
|
|
47
|
+
change_column: ->(visitor, args) { visitor.parse_change_column(args) },
|
|
48
|
+
remove_column: ->(visitor, args) { visitor.parse_remove_column(args) },
|
|
49
|
+
rename_column: ->(visitor, args) { visitor.parse_rename_column(args) },
|
|
50
|
+
add_index: ->(visitor, args) { visitor.parse_add_index(args) },
|
|
51
|
+
remove_index: ->(visitor, args) { visitor.parse_remove_index(args) },
|
|
52
|
+
rename_index: ->(visitor, args) { visitor.parse_rename_index(args) },
|
|
53
|
+
create_table: ->(visitor, args) { visitor.parse_create_table(args) },
|
|
54
|
+
drop_table: ->(visitor, args) { visitor.parse_drop_table(args) }
|
|
55
|
+
}.freeze
|
|
56
|
+
|
|
57
|
+
attr_reader :changes
|
|
58
|
+
|
|
59
|
+
def initialize
|
|
60
|
+
super
|
|
61
|
+
@changes = []
|
|
62
|
+
end
|
|
66
63
|
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
def visit_call_node(node)
|
|
65
|
+
arguments = node.arguments&.arguments
|
|
66
|
+
if node.block
|
|
67
|
+
return unless node.name == :create_table
|
|
69
68
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
change = parse_create_table_with_block(arguments, node.block)
|
|
70
|
+
@changes << change if change
|
|
71
|
+
elsif arguments && (parser = MAPPING[node.name])
|
|
72
|
+
change = parser.call(self, arguments)
|
|
73
|
+
@changes << change if change
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (parser = PARSER_MAPPING[method_name])
|
|
79
|
-
change = parser.call(args)
|
|
80
|
-
changes << change if change
|
|
75
|
+
return
|
|
76
|
+
end
|
|
77
|
+
super
|
|
81
78
|
end
|
|
82
79
|
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
def parse_add_column(args)
|
|
81
|
+
return unless args.size >= 3
|
|
85
82
|
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
{
|
|
84
|
+
action: :add_column,
|
|
85
|
+
table: sym_value(args[0]),
|
|
86
|
+
column: sym_value(args[1]),
|
|
87
|
+
type: sym_value(args[2]),
|
|
88
|
+
options: parse_hash(args[3])
|
|
89
|
+
}
|
|
90
|
+
end
|
|
88
91
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
table: sym_value(args[0]),
|
|
92
|
-
column: sym_value(args[1]),
|
|
93
|
-
type: sym_value(args[2]),
|
|
94
|
-
options: parse_hash(args[3])
|
|
95
|
-
}
|
|
96
|
-
end
|
|
92
|
+
def parse_change_column(args)
|
|
93
|
+
return unless args.size >= 3
|
|
97
94
|
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
{
|
|
96
|
+
action: :change_column,
|
|
97
|
+
table: sym_value(args[0]),
|
|
98
|
+
column: sym_value(args[1]),
|
|
99
|
+
type: sym_value(args[2]),
|
|
100
|
+
options: parse_hash(args[3])
|
|
101
|
+
}
|
|
102
|
+
end
|
|
100
103
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
table: sym_value(args[0]),
|
|
104
|
-
column: sym_value(args[1]),
|
|
105
|
-
type: sym_value(args[2]),
|
|
106
|
-
options: parse_hash(args[3])
|
|
107
|
-
}
|
|
108
|
-
end
|
|
104
|
+
def parse_remove_column(args)
|
|
105
|
+
return unless args.size >= 2
|
|
109
106
|
|
|
110
|
-
|
|
111
|
-
|
|
107
|
+
{
|
|
108
|
+
action: :remove_column,
|
|
109
|
+
table: sym_value(args[0]),
|
|
110
|
+
column: sym_value(args[1]),
|
|
111
|
+
options: parse_hash(args[2])
|
|
112
|
+
}
|
|
113
|
+
end
|
|
112
114
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
table: sym_value(args[0]),
|
|
116
|
-
column: sym_value(args[1]),
|
|
117
|
-
options: parse_hash(args[2])
|
|
118
|
-
}
|
|
119
|
-
end
|
|
115
|
+
def parse_rename_column(args)
|
|
116
|
+
return unless args.size >= 3
|
|
120
117
|
|
|
121
|
-
|
|
122
|
-
|
|
118
|
+
{
|
|
119
|
+
action: :rename_column,
|
|
120
|
+
table: sym_value(args[0]),
|
|
121
|
+
old_column: sym_value(args[1]),
|
|
122
|
+
new_column: sym_value(args[2])
|
|
123
|
+
}
|
|
124
|
+
end
|
|
123
125
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
table: sym_value(args[0]),
|
|
127
|
-
old_column: sym_value(args[1]),
|
|
128
|
-
new_column: sym_value(args[2])
|
|
129
|
-
}
|
|
130
|
-
end
|
|
126
|
+
def parse_add_index(args)
|
|
127
|
+
return unless args.size >= 2
|
|
131
128
|
|
|
132
|
-
|
|
133
|
-
|
|
129
|
+
{
|
|
130
|
+
action: :add_index,
|
|
131
|
+
table: sym_value(args[0]),
|
|
132
|
+
columns: array_or_single_value(args[1]),
|
|
133
|
+
options: parse_hash(args[2])
|
|
134
|
+
}
|
|
135
|
+
end
|
|
134
136
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
table: sym_value(args[0]),
|
|
138
|
-
columns: array_or_single_value(args[1]),
|
|
139
|
-
options: parse_hash(args[2])
|
|
140
|
-
}
|
|
141
|
-
end
|
|
137
|
+
def parse_remove_index(args)
|
|
138
|
+
return unless args.size >= 1
|
|
142
139
|
|
|
143
|
-
|
|
144
|
-
|
|
140
|
+
{
|
|
141
|
+
action: :remove_index,
|
|
142
|
+
table: sym_value(args[0]),
|
|
143
|
+
options: parse_hash(args[1])
|
|
144
|
+
}
|
|
145
|
+
end
|
|
145
146
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
table: sym_value(args[0]),
|
|
149
|
-
options: parse_hash(args[1])
|
|
150
|
-
}
|
|
151
|
-
end
|
|
147
|
+
def parse_rename_index(args)
|
|
148
|
+
return unless args.size >= 3
|
|
152
149
|
|
|
153
|
-
|
|
154
|
-
|
|
150
|
+
{
|
|
151
|
+
action: :rename_index,
|
|
152
|
+
table: sym_value(args[0]),
|
|
153
|
+
old_name: node_value(args[1]),
|
|
154
|
+
new_name: node_value(args[2])
|
|
155
|
+
}
|
|
156
|
+
end
|
|
155
157
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
table: sym_value(args[0]),
|
|
159
|
-
old_name: node_value(args[1]),
|
|
160
|
-
new_name: node_value(args[2])
|
|
161
|
-
}
|
|
162
|
-
end
|
|
158
|
+
def parse_create_table(args)
|
|
159
|
+
return unless args.size >= 1
|
|
163
160
|
|
|
164
|
-
|
|
165
|
-
|
|
161
|
+
{
|
|
162
|
+
action: :create_table,
|
|
163
|
+
table: sym_value(args[0]),
|
|
164
|
+
options: parse_hash(args[1])
|
|
165
|
+
}
|
|
166
|
+
end
|
|
166
167
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
table: sym_value(args[0]),
|
|
170
|
-
options: parse_hash(args[1])
|
|
171
|
-
}
|
|
172
|
-
end
|
|
168
|
+
def parse_drop_table(args)
|
|
169
|
+
return unless args.size >= 1
|
|
173
170
|
|
|
174
|
-
|
|
175
|
-
|
|
171
|
+
{
|
|
172
|
+
action: :drop_table,
|
|
173
|
+
table: sym_value(args[0]),
|
|
174
|
+
options: parse_hash(args[1])
|
|
175
|
+
}
|
|
176
|
+
end
|
|
176
177
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
178
|
+
def parse_create_table_with_block(args, block_node)
|
|
179
|
+
columns = parse_create_table_columns(block_node.body)
|
|
180
|
+
{
|
|
181
|
+
action: :create_table,
|
|
182
|
+
table: sym_value(args[0]),
|
|
183
|
+
options: parse_hash(args[1]),
|
|
184
|
+
columns: columns
|
|
185
|
+
}
|
|
186
|
+
end
|
|
183
187
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
columns = parse_create_table_columns(block_node.children[2])
|
|
187
|
-
{
|
|
188
|
-
action: :create_table,
|
|
189
|
-
table: sym_value(args[0]),
|
|
190
|
-
options: parse_hash(args[1]),
|
|
191
|
-
columns: columns
|
|
192
|
-
}
|
|
193
|
-
end
|
|
188
|
+
def parse_create_table_columns(statements_node)
|
|
189
|
+
return [] unless statements_node
|
|
194
190
|
|
|
195
|
-
|
|
196
|
-
|
|
191
|
+
statements_node.body.map { |node| parse_column_node(node) }.compact
|
|
192
|
+
end
|
|
197
193
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
end
|
|
194
|
+
def parse_column_node(node)
|
|
195
|
+
return unless node.is_a?(Prism::CallNode)
|
|
201
196
|
|
|
202
|
-
|
|
203
|
-
|
|
197
|
+
method = node.name
|
|
198
|
+
return parse_timestamps if method == :timestamps
|
|
204
199
|
|
|
205
|
-
|
|
206
|
-
|
|
200
|
+
arguments = node.arguments&.arguments
|
|
201
|
+
return unless arguments
|
|
207
202
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
203
|
+
{
|
|
204
|
+
column: sym_value(arguments[0]),
|
|
205
|
+
type: method,
|
|
206
|
+
options: parse_hash(arguments[1])
|
|
207
|
+
}
|
|
208
|
+
end
|
|
214
209
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
210
|
+
def parse_timestamps
|
|
211
|
+
[
|
|
212
|
+
{ column: :created_at, type: :datetime, options: { null: false } },
|
|
213
|
+
{ column: :updated_at, type: :datetime, options: { null: false } }
|
|
214
|
+
]
|
|
215
|
+
end
|
|
221
216
|
|
|
222
|
-
|
|
223
|
-
|
|
217
|
+
def sym_value(node)
|
|
218
|
+
return nil unless node.is_a?(Prism::SymbolNode)
|
|
224
219
|
|
|
225
|
-
|
|
226
|
-
|
|
220
|
+
node.value.to_sym
|
|
221
|
+
end
|
|
227
222
|
|
|
228
|
-
|
|
229
|
-
|
|
223
|
+
def array_or_single_value(node)
|
|
224
|
+
return [] unless node
|
|
230
225
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
226
|
+
if node.is_a?(Prism::ArrayNode)
|
|
227
|
+
node.elements.map { |child| node_value(child) }
|
|
228
|
+
else
|
|
229
|
+
node_value(node)
|
|
230
|
+
end
|
|
235
231
|
end
|
|
236
|
-
end
|
|
237
232
|
|
|
238
|
-
|
|
239
|
-
|
|
233
|
+
def parse_hash(node)
|
|
234
|
+
return {} unless node.is_a?(Prism::KeywordHashNode)
|
|
240
235
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
236
|
+
node.elements.each_with_object({}) do |assoc_node, result|
|
|
237
|
+
key_node = assoc_node.key
|
|
238
|
+
value_node = assoc_node.value
|
|
239
|
+
key = sym_value(key_node) || node_value(key_node)
|
|
240
|
+
value = node_value(value_node)
|
|
241
|
+
result[key] = value
|
|
242
|
+
end
|
|
246
243
|
end
|
|
247
|
-
end
|
|
248
244
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
245
|
+
def node_value(node)
|
|
246
|
+
return nil unless node
|
|
247
|
+
|
|
248
|
+
case node
|
|
249
|
+
when Prism::StringNode then node.content
|
|
250
|
+
when Prism::SymbolNode then node.value.to_sym
|
|
251
|
+
when Prism::IntegerNode then node.value
|
|
252
|
+
when Prism::TrueNode then true
|
|
253
|
+
when Prism::FalseNode then false
|
|
254
|
+
when Prism::NilNode then nil
|
|
255
|
+
else
|
|
256
|
+
node
|
|
257
|
+
end
|
|
259
258
|
end
|
|
260
259
|
end
|
|
261
260
|
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActualDbSchema
|
|
4
|
+
module Patches
|
|
5
|
+
# Patches Rails' CheckPending middleware to skip the pending
|
|
6
|
+
# migration check for ActualDbSchema UI routes (/rails/).
|
|
7
|
+
module CheckPending
|
|
8
|
+
def call(env)
|
|
9
|
+
return @app.call(env) if env["actual_db_schema.skip_pending_check"]
|
|
10
|
+
|
|
11
|
+
super
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "parser/ast/processor"
|
|
4
3
|
require "prism"
|
|
5
4
|
|
|
6
5
|
module ActualDbSchema
|
|
@@ -9,141 +8,121 @@ module ActualDbSchema
|
|
|
9
8
|
module_function
|
|
10
9
|
|
|
11
10
|
def parse_string(schema_content)
|
|
12
|
-
|
|
11
|
+
parse_result = Prism.parse(schema_content)
|
|
12
|
+
raise SyntaxError, "Schema is syntax invalid" unless parse_result.success?
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
visitor = SchemaVisitor.new
|
|
15
|
+
visitor.visit(parse_result.value)
|
|
16
|
+
visitor.schema
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
# Internal class used to
|
|
20
|
-
class
|
|
21
|
-
attr_reader :
|
|
19
|
+
# Internal class used to visit a create_table block
|
|
20
|
+
class CreateTableVisitor < Prism::Visitor
|
|
21
|
+
attr_reader :columns
|
|
22
22
|
|
|
23
|
-
def initialize
|
|
23
|
+
def initialize(block_arg)
|
|
24
24
|
super()
|
|
25
|
-
@
|
|
25
|
+
@block_arg = block_arg
|
|
26
|
+
@columns = {}
|
|
26
27
|
end
|
|
27
28
|
|
|
28
|
-
def
|
|
29
|
-
|
|
29
|
+
def visit_call_node(node)
|
|
30
|
+
return unless node.receiver.is_a?(Prism::LocalVariableReadNode) && node.receiver.name == @block_arg
|
|
30
31
|
|
|
31
|
-
if
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
if node.name == :timestamps
|
|
33
|
+
name = "timestamps"
|
|
34
|
+
options = {}
|
|
35
|
+
else
|
|
36
|
+
name_arg, *, keyword_args = node.arguments&.arguments
|
|
37
|
+
name = extract_column_name(name_arg)
|
|
38
|
+
options = extract_column_options(keyword_args)
|
|
35
39
|
end
|
|
40
|
+
return unless name
|
|
36
41
|
|
|
37
|
-
|
|
42
|
+
@columns[name] = { type: node.message.to_sym, options: options }
|
|
38
43
|
end
|
|
39
44
|
|
|
40
|
-
def
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
def extract_column_name(node)
|
|
46
|
+
case node
|
|
47
|
+
when Prism::StringNode
|
|
48
|
+
node.content
|
|
49
|
+
when Prism::SymbolNode
|
|
50
|
+
node.value
|
|
45
51
|
end
|
|
46
|
-
|
|
47
|
-
super
|
|
48
52
|
end
|
|
49
53
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def create_table_call?(node)
|
|
53
|
-
return false unless node.is_a?(Parser::AST::Node)
|
|
54
|
-
|
|
55
|
-
_receiver, method_name, *_args = node.children
|
|
56
|
-
method_name == :create_table
|
|
57
|
-
end
|
|
54
|
+
def extract_column_options(node)
|
|
55
|
+
return {} unless node.is_a?(Prism::KeywordHashNode)
|
|
58
56
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
options = {}
|
|
58
|
+
node.elements.each do |assoc_node|
|
|
59
|
+
next unless assoc_node.is_a?(Prism::AssocNode)
|
|
60
|
+
next unless (key = extract_key(assoc_node.key))
|
|
62
61
|
|
|
63
|
-
|
|
64
|
-
when :str then table_arg.children.first
|
|
65
|
-
when :sym then table_arg.children.first.to_s
|
|
62
|
+
options[key] = extract_literal(assoc_node.value)
|
|
66
63
|
end
|
|
64
|
+
options
|
|
67
65
|
end
|
|
68
66
|
|
|
69
|
-
def
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
columns = {}
|
|
75
|
-
children.each do |expr|
|
|
76
|
-
col = process_column_node(expr)
|
|
77
|
-
columns[col[:name]] = { type: col[:type], options: col[:options] } if col && col[:name]
|
|
67
|
+
def extract_key(node)
|
|
68
|
+
case node
|
|
69
|
+
when Prism::SymbolNode then node.value.to_sym
|
|
70
|
+
when Prism::StringNode then node.content.to_sym
|
|
78
71
|
end
|
|
79
|
-
columns
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def process_column_node(node)
|
|
83
|
-
return unless node.is_a?(Parser::AST::Node)
|
|
84
|
-
return unless node.type == :send
|
|
85
|
-
|
|
86
|
-
receiver, method_name, column_node, *args = node.children
|
|
87
|
-
|
|
88
|
-
return unless receiver && receiver.type == :lvar
|
|
89
|
-
|
|
90
|
-
return { name: "timestamps", type: :timestamps, options: {} } if method_name == :timestamps
|
|
91
|
-
|
|
92
|
-
col_name = extract_column_name(column_node)
|
|
93
|
-
options = extract_column_options(args)
|
|
94
|
-
|
|
95
|
-
{ name: col_name, type: method_name, options: options }
|
|
96
72
|
end
|
|
97
73
|
|
|
98
|
-
def
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
when
|
|
103
|
-
when
|
|
74
|
+
def extract_literal(node)
|
|
75
|
+
case node
|
|
76
|
+
when Prism::TrueNode then true
|
|
77
|
+
when Prism::FalseNode then false
|
|
78
|
+
when Prism::IntegerNode then node.value
|
|
79
|
+
when Prism::SymbolNode then node.value.to_sym
|
|
80
|
+
when Prism::StringNode then node.content
|
|
104
81
|
end
|
|
105
82
|
end
|
|
83
|
+
end
|
|
106
84
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
next unless arg && arg.type == :hash
|
|
85
|
+
# Internal class used to process the AST and collect schema information.
|
|
86
|
+
class SchemaVisitor < Prism::Visitor
|
|
87
|
+
attr_reader :schema
|
|
111
88
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
89
|
+
def initialize
|
|
90
|
+
super
|
|
91
|
+
@schema = {}
|
|
115
92
|
end
|
|
116
93
|
|
|
117
|
-
def
|
|
118
|
-
|
|
119
|
-
|
|
94
|
+
def visit_call_node(node)
|
|
95
|
+
if node.name == :create_table
|
|
96
|
+
return unless node.block
|
|
97
|
+
|
|
98
|
+
table_name = extract_table_name(node)
|
|
99
|
+
return unless table_name
|
|
120
100
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
value = extract_literal(value_node)
|
|
125
|
-
hash[key] = value
|
|
101
|
+
columns = extract_columns(node.block)
|
|
102
|
+
@schema[table_name] = columns
|
|
103
|
+
return
|
|
126
104
|
end
|
|
127
|
-
|
|
105
|
+
super
|
|
128
106
|
end
|
|
129
107
|
|
|
130
|
-
|
|
131
|
-
return unless node.is_a?(Parser::AST::Node)
|
|
108
|
+
private
|
|
132
109
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
110
|
+
def extract_table_name(call_node)
|
|
111
|
+
first_arg = call_node.arguments&.arguments&.first
|
|
112
|
+
case first_arg
|
|
113
|
+
when Prism::StringNode
|
|
114
|
+
first_arg.content
|
|
115
|
+
when Prism::SymbolNode
|
|
116
|
+
first_arg.value
|
|
136
117
|
end
|
|
137
118
|
end
|
|
138
119
|
|
|
139
|
-
def
|
|
140
|
-
return unless
|
|
120
|
+
def extract_columns(block_node)
|
|
121
|
+
return unless (block_arg = block_node.locals&.first)
|
|
141
122
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
when false then false
|
|
146
|
-
end
|
|
123
|
+
v = CreateTableVisitor.new(block_arg)
|
|
124
|
+
v.visit(block_node)
|
|
125
|
+
v.columns
|
|
147
126
|
end
|
|
148
127
|
end
|
|
149
128
|
end
|
data/lib/actual_db_schema.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "actual_db_schema/middlewares/skip_pending_migration_check"
|
|
3
4
|
require "actual_db_schema/engine"
|
|
4
5
|
require "active_record/migration"
|
|
5
6
|
require "csv"
|
|
@@ -14,6 +15,7 @@ require_relative "actual_db_schema/failed_migration"
|
|
|
14
15
|
require_relative "actual_db_schema/migration_context"
|
|
15
16
|
require_relative "actual_db_schema/migration_parser"
|
|
16
17
|
require_relative "actual_db_schema/output_formatter"
|
|
18
|
+
require_relative "actual_db_schema/patches/check_pending"
|
|
17
19
|
require_relative "actual_db_schema/patches/migration_proxy"
|
|
18
20
|
require_relative "actual_db_schema/patches/migrator"
|
|
19
21
|
require_relative "actual_db_schema/patches/migration_context"
|
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.
|
|
4
|
+
version: 0.10.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: 2026-
|
|
11
|
+
date: 2026-10-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|
|
@@ -38,20 +38,6 @@ dependencies:
|
|
|
38
38
|
- - ">="
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
40
|
version: '0'
|
|
41
|
-
- !ruby/object:Gem::Dependency
|
|
42
|
-
name: ast
|
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
|
44
|
-
requirements:
|
|
45
|
-
- - ">="
|
|
46
|
-
- !ruby/object:Gem::Version
|
|
47
|
-
version: '0'
|
|
48
|
-
type: :runtime
|
|
49
|
-
prerelease: false
|
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
-
requirements:
|
|
52
|
-
- - ">="
|
|
53
|
-
- !ruby/object:Gem::Version
|
|
54
|
-
version: '0'
|
|
55
41
|
- !ruby/object:Gem::Dependency
|
|
56
42
|
name: csv
|
|
57
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -66,20 +52,6 @@ dependencies:
|
|
|
66
52
|
- - ">="
|
|
67
53
|
- !ruby/object:Gem::Version
|
|
68
54
|
version: '0'
|
|
69
|
-
- !ruby/object:Gem::Dependency
|
|
70
|
-
name: parser
|
|
71
|
-
requirement: !ruby/object:Gem::Requirement
|
|
72
|
-
requirements:
|
|
73
|
-
- - ">="
|
|
74
|
-
- !ruby/object:Gem::Version
|
|
75
|
-
version: '0'
|
|
76
|
-
type: :runtime
|
|
77
|
-
prerelease: false
|
|
78
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
-
requirements:
|
|
80
|
-
- - ">="
|
|
81
|
-
- !ruby/object:Gem::Version
|
|
82
|
-
version: '0'
|
|
83
55
|
- !ruby/object:Gem::Dependency
|
|
84
56
|
name: prism
|
|
85
57
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -151,9 +123,8 @@ dependencies:
|
|
|
151
123
|
- !ruby/object:Gem::Version
|
|
152
124
|
version: '0'
|
|
153
125
|
description: |
|
|
154
|
-
Keep
|
|
155
|
-
|
|
156
|
-
Stop wasting hours on DB maintenance locally, CI, staging/sandbox, or even production.
|
|
126
|
+
Keep Rails databases in sync across Git branches. Automatically roll back stale migrations,
|
|
127
|
+
prevent database errors and unwanted schema diffs, and inspect migration history with a web dashboard.
|
|
157
128
|
email:
|
|
158
129
|
- ka8725@gmail.com
|
|
159
130
|
executables: []
|
|
@@ -202,11 +173,13 @@ files:
|
|
|
202
173
|
- lib/actual_db_schema/git.rb
|
|
203
174
|
- lib/actual_db_schema/git_hooks.rb
|
|
204
175
|
- lib/actual_db_schema/instrumentation.rb
|
|
176
|
+
- lib/actual_db_schema/middlewares/skip_pending_migration_check.rb
|
|
205
177
|
- lib/actual_db_schema/migration.rb
|
|
206
178
|
- lib/actual_db_schema/migration_context.rb
|
|
207
179
|
- lib/actual_db_schema/migration_parser.rb
|
|
208
180
|
- lib/actual_db_schema/multi_tenant.rb
|
|
209
181
|
- lib/actual_db_schema/output_formatter.rb
|
|
182
|
+
- lib/actual_db_schema/patches/check_pending.rb
|
|
210
183
|
- lib/actual_db_schema/patches/migration_context.rb
|
|
211
184
|
- lib/actual_db_schema/patches/migration_proxy.rb
|
|
212
185
|
- lib/actual_db_schema/patches/migrator.rb
|
|
@@ -257,6 +230,6 @@ requirements: []
|
|
|
257
230
|
rubygems_version: 3.3.26
|
|
258
231
|
signing_key:
|
|
259
232
|
specification_version: 4
|
|
260
|
-
summary: Keep
|
|
233
|
+
summary: Keep Rails databases in sync across Git branches.
|
|
261
234
|
test_files: []
|
|
262
235
|
...
|