actual_db_schema 0.7.5 → 0.7.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a49295fefc3c066ebb4ba22ea2304a25013963ce7daf614d97032ec249d2769a
4
- data.tar.gz: bb870c91a0896aaf35d304869f2f6af2d9dc190e8d8694ff9a56b56d8c693bc0
3
+ metadata.gz: a731a134ac425aed56e92b8c715812d17c9c1b3d519580281f7284dd05ae8d4a
4
+ data.tar.gz: e262bafa9ebc123fc702baa883fae55ac30a030d778ef9123f1f88cddb6b9192
5
5
  SHA512:
6
- metadata.gz: ecffe8659acb2add81d97c36418ae6b7f5bb65c053f44856e4da3da9a4c91911e0016bb3fa8c5fbbd463816c45c1fdef63a5ae58fa2b2b4fdb864a47c1946f3a
7
- data.tar.gz: cb823af551359b10b97dd3e0d3f51966c076f95ee8b16fcc9d2b1d335348301e17185a8df2e83b62c0a8887caea841b9d8b9df37446de5a36912038a4d518441
6
+ metadata.gz: 03aecc678b4de3e808367715de4c7d193ebab7fb036fbba31b0d0da771637b165bc348ad08bce50dd6a5472701065e8888b9aecbed042a282627fa792260ba39
7
+ data.tar.gz: 81fa7c7b457be66ab587fa1062a61236fb5e578060b6565a2d26f1eaebd6817bdad09e6ec2b15ffbd8a5d489d84b42463fd02689bc8257a0e308f2cd6987e0ce
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## [0.7.6] - 2024-07-22
2
+ - Added UI
3
+ - Added environment variable `ACTUAL_DB_SCHEMA_UI_ENABLED` to enable/disable the UI in specific environments
4
+ - Added configuration option `ActualDbSchema.config[:ui_enabled]` to enable/disable the UI in specific environments
5
+
1
6
  ## [0.7.5] - 2024-06-20
2
7
  - Added db:rollback_migrations:manual task to manually rolls back phantom migrations one by one
3
8
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- actual_db_schema (0.7.5)
4
+ actual_db_schema (0.7.6)
5
5
  activerecord (>= 6.0.0)
6
6
  activesupport (>= 6.0.0)
7
7
  csv
data/README.md CHANGED
@@ -62,6 +62,35 @@ The gem offers the following rake tasks that can be manually run according to yo
62
62
  - `rails db:rollback_branches:manual` - run it to manually rolls back phantom migrations one by one.
63
63
  - `rails db:phantom_migrations` - displays a list of phantom migrations.
64
64
 
65
+ ## Accessing the UI
66
+
67
+ The UI for managing migrations is enabled automatically. To access the UI, simply navigate to the following URL in your web browser:
68
+ ```
69
+ http://localhost:3000/rails/phantom_migrations
70
+ ```
71
+ This page displays a list of phantom migrations for each database connection and provides options to view details and rollback them.
72
+
73
+ ## UI options
74
+
75
+ By default, the UI is enabled in the development environment. If you prefer to enable the UI for another environment, you can do so in two ways:
76
+
77
+ ### 1. Using Environment Variable
78
+
79
+ Set the environment variable `ACTUAL_DB_SCHEMA_UI_ENABLED` to `true`:
80
+
81
+ ```sh
82
+ export ACTUAL_DB_SCHEMA_UI_ENABLED=true
83
+ ```
84
+
85
+ ### 2. Using Initializer
86
+ Add the following line to your initializer file (`config/initializers/actual_db_schema.rb`):
87
+
88
+ ```ruby
89
+ ActualDbSchema.config[:ui_enabled] = true
90
+ ```
91
+
92
+ > With this option, the UI can be disabled for all environments or be enabled in specific ones.
93
+
65
94
  ## Disabling Automatic Rollback
66
95
 
67
96
  By default, the automatic rollback of migrations is enabled. If you prefer to perform manual rollbacks, you can disable the automatic rollback in two ways:
@@ -0,0 +1,39 @@
1
+ document.addEventListener('DOMContentLoaded', function () {
2
+ const migrationActions = document.querySelectorAll('.migration-action');
3
+
4
+ migrationActions.forEach(button => {
5
+ button.addEventListener('click', function (event) {
6
+ const originalText = button.value;
7
+ button.value = 'Loading...';
8
+ disableButtons();
9
+
10
+ fetch(event.target.form.action, { method: 'POST'})
11
+ .then(response => {
12
+ if (response.ok) {
13
+ window.location.reload();
14
+ } else {
15
+ throw new Error('Network response was not ok.');
16
+ }
17
+ })
18
+ .catch(error => {
19
+ console.error('There has been a problem with your fetch operation:', error);
20
+ enableButtons();
21
+ button.value = originalText;
22
+ });
23
+
24
+ event.preventDefault();
25
+ });
26
+ });
27
+
28
+ function disableButtons() {
29
+ migrationActions.forEach(button => {
30
+ button.disabled = true;
31
+ });
32
+ }
33
+
34
+ function enableButtons() {
35
+ migrationActions.forEach(button => {
36
+ button.disabled = false;
37
+ });
38
+ }
39
+ });
@@ -0,0 +1,114 @@
1
+ body {
2
+ margin: 8px;
3
+ background-color: #fff;
4
+ color: #333;
5
+ }
6
+
7
+ body, p, td {
8
+ font-family: helvetica, verdana, arial, sans-serif;
9
+ font-size: 13px;
10
+ line-height: 18px;
11
+ }
12
+
13
+ h2 {
14
+ padding-left: 10px;
15
+ }
16
+
17
+ table {
18
+ margin: 0;
19
+ border-collapse: collapse;
20
+
21
+ thead tr {
22
+ border-bottom: 2px solid #ddd;
23
+ }
24
+
25
+ tbody {
26
+ .migration-row.phantom {
27
+ background-color: #fff3f3;
28
+ }
29
+
30
+ .migration-row.normal {
31
+ background-color: #ffffff;
32
+ }
33
+
34
+ .migration-row:nth-child(odd).phantom {
35
+ background-color: #ffe6e6;
36
+ }
37
+
38
+ .migration-row:nth-child(odd).normal {
39
+ background-color: #f9f9f9;
40
+ }
41
+ }
42
+
43
+ td {
44
+ padding: 14px 30px;
45
+ }
46
+ }
47
+
48
+ .top-buttons {
49
+ margin: 8px;
50
+ display: flex;
51
+ align-items: center;
52
+
53
+ .top-button {
54
+ background-color: #ddd;
55
+ }
56
+ }
57
+
58
+ .button, .top-button {
59
+ font-weight: bold;
60
+ color: #000;
61
+ border: none;
62
+ padding: 5px 10px;
63
+ text-align: center;
64
+ text-decoration: none;
65
+ display: inline-block;
66
+ margin: 0 2px;
67
+ cursor: pointer;
68
+ border-radius: 4px;
69
+ transition: background-color 0.3s;
70
+ background: none;
71
+ }
72
+
73
+ .button:hover, .top-button:hover {
74
+ color: #fff;
75
+ background-color: #000;
76
+ }
77
+
78
+ .button:disabled, .button:hover:disabled {
79
+ background-color: transparent;
80
+ color: #666;
81
+ cursor: not-allowed;
82
+ }
83
+
84
+ .button-container {
85
+ display: flex;
86
+ }
87
+
88
+ pre {
89
+ background-color: #f7f7f7;
90
+ padding: 10px;
91
+ border: 1px solid #ddd;
92
+ }
93
+
94
+ .truncate-text {
95
+ max-width: 200px;
96
+ overflow: hidden;
97
+ text-overflow: ellipsis;
98
+ }
99
+
100
+ .flash {
101
+ padding: 10px;
102
+ margin-bottom: 10px;
103
+ border-radius: 5px;
104
+ }
105
+
106
+ .flash.notice {
107
+ background-color: #d4edda;
108
+ color: #155724;
109
+ }
110
+
111
+ .flash.alert {
112
+ background-color: #f8d7da;
113
+ color: #721c24;
114
+ }
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActualDbSchema
4
+ # Controller to display the list of migrations for each database connection.
5
+ class MigrationsController < ActionController::Base
6
+ protect_from_forgery with: :exception
7
+ skip_before_action :verify_authenticity_token
8
+
9
+ def index; end
10
+
11
+ def show
12
+ render file: "#{Rails.root}/public/404.html", layout: false, status: :not_found unless migration
13
+ end
14
+
15
+ def rollback
16
+ handle_rollback(params[:id], params[:database])
17
+ redirect_to migrations_path
18
+ end
19
+
20
+ def migrate
21
+ handle_migrate(params[:id], params[:database])
22
+ redirect_to migrations_path
23
+ end
24
+
25
+ private
26
+
27
+ def handle_rollback(id, database)
28
+ ActualDbSchema::Migration.instance.rollback(id, database)
29
+ flash[:notice] = "Migration #{id} was successfully rolled back."
30
+ rescue StandardError => e
31
+ flash[:alert] = e.message
32
+ end
33
+
34
+ def handle_migrate(id, database)
35
+ ActualDbSchema::Migration.instance.migrate(id, database)
36
+ flash[:notice] = "Migration #{id} was successfully migrated."
37
+ rescue StandardError => e
38
+ flash[:alert] = e.message
39
+ end
40
+
41
+ helper_method def migrations
42
+ @migrations ||= ActualDbSchema::Migration.instance.all
43
+ end
44
+
45
+ helper_method def migration
46
+ @migration ||= ActualDbSchema::Migration.instance.find(params[:id], params[:database])
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActualDbSchema
4
+ # Controller to display the list of phantom migrations for each database connection.
5
+ class PhantomMigrationsController < ActionController::Base
6
+ protect_from_forgery with: :exception
7
+ skip_before_action :verify_authenticity_token
8
+
9
+ def index; end
10
+
11
+ def show
12
+ render file: "#{Rails.root}/public/404.html", layout: false, status: :not_found unless phantom_migration
13
+ end
14
+
15
+ def rollback
16
+ handle_rollback(params[:id], params[:database])
17
+ redirect_to phantom_migrations_path
18
+ end
19
+
20
+ def rollback_all
21
+ handle_rollback_all
22
+ redirect_to phantom_migrations_path
23
+ end
24
+
25
+ private
26
+
27
+ def handle_rollback(id, database)
28
+ ActualDbSchema::Migration.instance.rollback(id, database)
29
+ flash[:notice] = "Migration #{id} was successfully rolled back."
30
+ rescue StandardError => e
31
+ flash[:alert] = e.message
32
+ end
33
+
34
+ def handle_rollback_all
35
+ ActualDbSchema::Migration.instance.rollback_all
36
+ flash[:notice] = "Migrations was successfully rolled back."
37
+ rescue StandardError => e
38
+ flash[:alert] = e.message
39
+ end
40
+
41
+ helper_method def phantom_migrations
42
+ @phantom_migrations ||= ActualDbSchema::Migration.instance.all_phantom
43
+ end
44
+
45
+ helper_method def phantom_migration
46
+ @phantom_migration ||= ActualDbSchema::Migration.instance.find(params[:id], params[:database])
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,70 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Migrations</title>
5
+ <%= stylesheet_link_tag 'styles', media: 'all' %>
6
+ <%= javascript_include_tag 'application' %>
7
+ </head>
8
+ <body>
9
+ <div>
10
+ <% flash.each do |key, message| %>
11
+ <div class="flash <%= key %>"><%= message %></div>
12
+ <% end %>
13
+ <h2>Migrations</h2>
14
+ <p>
15
+ <span style="background-color: #ffe6e6; padding: 0 5px;">Red rows</span> represent phantom migrations.
16
+ </p>
17
+ <div class="top-buttons">
18
+ <%= link_to 'Phantom Migrations', phantom_migrations_path, class: "top-button" %>
19
+ </div>
20
+ <% if migrations.present? %>
21
+ <table>
22
+ <thead>
23
+ <tr>
24
+ <th>Status</th>
25
+ <th>Migration ID</th>
26
+ <th>Name</th>
27
+ <th>Branch</th>
28
+ <th>Database</th>
29
+ <th>Actions</th>
30
+ </tr>
31
+ </thead>
32
+ <tbody>
33
+ <% migrations.each do |migration| %>
34
+ <tr class="migration-row <%= migration[:phantom] ? 'phantom' : 'normal' %>">
35
+ <td><%= migration[:status] %></td>
36
+ <td><%= migration[:version] %></td>
37
+ <td>
38
+ <div class="truncate-text" title="<%= migration[:name] %>">
39
+ <%= migration[:name] %>
40
+ </div>
41
+ </td>
42
+ <td><%= migration[:branch] %></td>
43
+ <td><%= migration[:database] %></td>
44
+ <td>
45
+ <div class='button-container'>
46
+ <%= link_to '👁 Show',
47
+ migration_path(id: migration[:version], database: migration[:database]),
48
+ class: 'button' %>
49
+ <%= button_to '⎌ Rollback',
50
+ rollback_migration_path(id: migration[:version], database: migration[:database]),
51
+ method: :post,
52
+ class: 'button migration-action',
53
+ style: ('display: none;' if migration[:status] == "down") %>
54
+ <%= button_to '⬆ Migrate',
55
+ migrate_migration_path(id: migration[:version], database: migration[:database]),
56
+ method: :post,
57
+ class: 'button migration-action',
58
+ style: ('display: none;' if migration[:status] == "up" || migration[:phantom]) %>
59
+ </div>
60
+ </td>
61
+ </tr>
62
+ <% end %>
63
+ </tbody>
64
+ </table>
65
+ <% else %>
66
+ <p>No migrations found.</p>
67
+ <% end %>
68
+ </div>
69
+ </body>
70
+ </html>
@@ -0,0 +1,58 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Migration Details</title>
5
+ <%= stylesheet_link_tag 'styles', media: 'all' %>
6
+ <%= javascript_include_tag 'application' %>
7
+ </head>
8
+ <body>
9
+ <div>
10
+ <% flash.each do |key, message| %>
11
+ <div class="flash <%= key %>"><%= message %></div>
12
+ <% end %>
13
+ <h2>Migration <%= migration[:name] %> Details</h2>
14
+ <table>
15
+ <tbody>
16
+ <tr>
17
+ <th>Status</th>
18
+ <td><%= migration[:status] %></td>
19
+ </tr>
20
+ <tr>
21
+ <th>Migration ID</th>
22
+ <td><%= migration[:version] %></td>
23
+ </tr>
24
+ <tr>
25
+ <th>Branch</th>
26
+ <td><%= migration[:branch] %></td>
27
+ </tr>
28
+ <tr>
29
+ <th>Database</th>
30
+ <td><%= migration[:database] %></td>
31
+ </tr>
32
+ <tr>
33
+ <th>Path</th>
34
+ <td><%= migration[:filename] %></td>
35
+ </tr>
36
+ </tbody>
37
+ </table>
38
+
39
+ <h3>Migration Code</h3>
40
+ <div>
41
+ <pre><%= File.read(migration[:filename]) %></pre>
42
+ </div>
43
+ <div class='button-container'>
44
+ <%= link_to '← Back', migrations_path, class: 'button' %>
45
+ <%= button_to '⎌ Rollback',
46
+ rollback_migration_path(id: migration[:version], database: migration[:database]),
47
+ method: :post,
48
+ class: 'button migration-action',
49
+ style: ('display: none;' if migration[:status] == "down") %>
50
+ <%= button_to '⬆ Migrate',
51
+ migrate_migration_path(id: migration[:version], database: migration[:database]),
52
+ method: :post,
53
+ class: 'button migration-action',
54
+ style: ('display: none;' if migration[:status] == "up" || migration[:phantom]) %>
55
+ </div>
56
+ </div>
57
+ </body>
58
+ </html>
@@ -0,0 +1,67 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Phantom Migrations</title>
5
+ <%= stylesheet_link_tag 'styles', media: 'all' %>
6
+ <%= javascript_include_tag 'application' %>
7
+ </head>
8
+ <body>
9
+ <div>
10
+ <% flash.each do |key, message| %>
11
+ <div class="flash <%= key %>"><%= message %></div>
12
+ <% end %>
13
+ <h2>Phantom Migrations</h2>
14
+ <div class="top-buttons">
15
+ <%= link_to 'All Migrations', migrations_path, class: "top-button" %>
16
+ <% if phantom_migrations.present? %>
17
+ <%= button_to '⎌ Rollback all',
18
+ rollback_all_phantom_migrations_path,
19
+ method: :post,
20
+ class: 'button migration-action' %>
21
+ <% end %>
22
+ </div>
23
+ <% if phantom_migrations.present? %>
24
+ <table>
25
+ <thead>
26
+ <tr>
27
+ <th>Status</th>
28
+ <th>Migration ID</th>
29
+ <th>Name</th>
30
+ <th>Branch</th>
31
+ <th>Database</th>
32
+ <th>Actions</th>
33
+ </tr>
34
+ </thead>
35
+ <tbody>
36
+ <% phantom_migrations.each do |migration| %>
37
+ <tr class="migration-row phantom">
38
+ <td><%= migration[:status] %></td>
39
+ <td><%= migration[:version] %></td>
40
+ <td>
41
+ <div class="truncate-text" title="<%= migration[:name] %>">
42
+ <%= migration[:name] %>
43
+ </div>
44
+ </td>
45
+ <td><%= migration[:branch] %></td>
46
+ <td><%= migration[:database] %></td>
47
+ <td>
48
+ <div class='button-container'>
49
+ <%= link_to '👁 Show',
50
+ phantom_migration_path(id: migration[:version], database: migration[:database]),
51
+ class: 'button' %>
52
+ <%= button_to '⎌ Rollback',
53
+ rollback_phantom_migration_path(id: migration[:version], database: migration[:database]),
54
+ method: :post,
55
+ class: 'button migration-action' %>
56
+ </div>
57
+ </td>
58
+ </tr>
59
+ <% end %>
60
+ </tbody>
61
+ </table>
62
+ <% else %>
63
+ <p>No phantom migrations found.</p>
64
+ <% end %>
65
+ </div>
66
+ </body>
67
+ </html>
@@ -0,0 +1,52 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Phantom Migration Details</title>
5
+ <%= stylesheet_link_tag 'styles', media: 'all' %>
6
+ <%= javascript_include_tag 'application' %>
7
+ </head>
8
+ <body>
9
+ <div>
10
+ <% flash.each do |key, message| %>
11
+ <div class="flash <%= key %>"><%= message %></div>
12
+ <% end %>
13
+ <h2>Phantom Migration <%= phantom_migration[:name] %> Details</h2>
14
+ <table>
15
+ <tbody>
16
+ <tr>
17
+ <th>Status</th>
18
+ <td><%= phantom_migration[:status] %></td>
19
+ </tr>
20
+ <tr>
21
+ <th>Migration ID</th>
22
+ <td><%= phantom_migration[:version] %></td>
23
+ </tr>
24
+ <tr>
25
+ <th>Branch</th>
26
+ <td><%= phantom_migration[:branch] %></td>
27
+ </tr>
28
+ <tr>
29
+ <th>Database</th>
30
+ <td><%= phantom_migration[:database] %></td>
31
+ </tr>
32
+ <tr>
33
+ <th>Path</th>
34
+ <td><%= phantom_migration[:filename] %></td>
35
+ </tr>
36
+ </tbody>
37
+ </table>
38
+
39
+ <h3>Migration Code</h3>
40
+ <div>
41
+ <pre><%= File.read(phantom_migration[:filename]) %></pre>
42
+ </div>
43
+ <div class='button-container'>
44
+ <%= link_to '← Back', phantom_migrations_path, class: 'button' %>
45
+ <%= button_to '⎌ Rollback',
46
+ rollback_phantom_migration_path(id: params[:id], database: params[:database]),
47
+ method: :post,
48
+ class: 'button migration-action' %>
49
+ </div>
50
+ </div>
51
+ </body>
52
+ </html>
data/config/routes.rb ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ ActualDbSchema::Engine.routes.draw do
4
+ resources :migrations, only: %i[index show] do
5
+ member do
6
+ post :rollback
7
+ post :migrate
8
+ end
9
+ end
10
+ resources :phantom_migrations, only: %i[index show] do
11
+ member do
12
+ post :rollback
13
+ end
14
+ collection do
15
+ post :rollback_all
16
+ end
17
+ end
18
+ end
@@ -4,6 +4,12 @@ module ActualDbSchema
4
4
  module Commands
5
5
  # Base class for all commands
6
6
  class Base
7
+ attr_reader :context
8
+
9
+ def initialize(context)
10
+ @context = context
11
+ end
12
+
7
13
  def call
8
14
  unless ActualDbSchema.config.fetch(:enabled, true)
9
15
  raise "ActualDbSchema is disabled. Set ActualDbSchema.config[:enabled] = true to enable it."
@@ -17,22 +23,6 @@ module ActualDbSchema
17
23
  def call_impl
18
24
  raise NotImplementedError
19
25
  end
20
-
21
- def context
22
- @context ||= fetch_migration_context.tap do |c|
23
- c.extend(ActualDbSchema::Patches::MigrationContext)
24
- end
25
- end
26
-
27
- def fetch_migration_context
28
- ar_version = Gem::Version.new(ActiveRecord::VERSION::STRING)
29
- if ar_version >= Gem::Version.new("7.2.0") ||
30
- (ar_version >= Gem::Version.new("7.1.0") && ar_version.prerelease?)
31
- ActiveRecord::Base.connection_pool.migration_context
32
- else
33
- ActiveRecord::Base.connection.migration_context
34
- end
35
- end
36
26
  end
37
27
  end
38
28
  end
@@ -12,27 +12,18 @@ module ActualDbSchema
12
12
  end
13
13
 
14
14
  def indexed_phantom_migrations
15
- @indexed_phantom_migrations ||= context.migrations.index_by { |m| m.version.to_s }
15
+ @indexed_phantom_migrations ||= context.phantom_migrations.index_by { |m| m.version.to_s }
16
16
  end
17
17
 
18
18
  def preambule
19
19
  puts "\nPhantom migrations\n\n"
20
20
  puts "Below is a list of irrelevant migrations executed in unmerged branches."
21
21
  puts "To bring your database schema up to date, the migrations marked as \"up\" should be rolled back."
22
- database_path = db_config[:database]
23
- puts "\ndatabase: #{database_path}\n\n"
22
+ puts "\ndatabase: #{ActualDbSchema.db_config[:database]}\n\n"
24
23
  puts header.join(" ")
25
24
  puts "-" * separator_width
26
25
  end
27
26
 
28
- def db_config
29
- if ActiveRecord::Base.respond_to?(:connection_db_config)
30
- ActiveRecord::Base.connection_db_config.configuration_hash
31
- else
32
- ActiveRecord::Base.connection_config
33
- end
34
- end
35
-
36
27
  def separator_width
37
28
  header.map(&:length).sum + (header.size - 1) * 2
38
29
  end
@@ -66,14 +57,14 @@ module ActualDbSchema
66
57
  ].join(" ")
67
58
  end
68
59
 
69
- def branch_for(version)
70
- metadata.fetch(version, {})[:branch] || "unknown"
71
- end
72
-
73
60
  def metadata
74
61
  @metadata ||= ActualDbSchema::Store.instance.read
75
62
  end
76
63
 
64
+ def branch_for(version)
65
+ metadata.fetch(version, {})[:branch] || "unknown"
66
+ end
67
+
77
68
  def longest_branch_name
78
69
  @longest_branch_name ||=
79
70
  metadata.values.map { |v| v[:branch] }.compact.max_by(&:length) || "unknown"
@@ -4,9 +4,9 @@ module ActualDbSchema
4
4
  module Commands
5
5
  # Rolls back all phantom migrations
6
6
  class Rollback < Base
7
- def initialize(manual_mode: false)
7
+ def initialize(context, manual_mode: false)
8
8
  @manual_mode = manual_mode || manual_mode_default?
9
- super()
9
+ super(context)
10
10
  end
11
11
 
12
12
  private
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActualDbSchema
4
+ # It isolates the namespace to avoid conflicts with the main application.
5
+ class Engine < ::Rails::Engine
6
+ isolate_namespace ActualDbSchema
7
+
8
+ initializer "actual_db_schema.initialize" do |app|
9
+ if ActualDbSchema.config[:ui_enabled]
10
+ app.routes.append do
11
+ mount ActualDbSchema::Engine => "/rails"
12
+ end
13
+
14
+ app.config.assets.precompile += %w[styles.css application.js]
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActualDbSchema
4
+ # The Migration class is responsible for managing and retrieving migration information
5
+ class Migration
6
+ include Singleton
7
+
8
+ Migration = Struct.new(:status, :version, :name, :branch, :database, :filename, :phantom, keyword_init: true)
9
+
10
+ def all_phantom
11
+ migrations = []
12
+
13
+ MigrationContext.instance.each do |context|
14
+ indexed_migrations = context.phantom_migrations.index_by { |m| m.version.to_s }
15
+
16
+ context.migrations_status.each do |status, version|
17
+ migration = indexed_migrations[version]
18
+ migrations << build_migration_struct(status, migration) if should_include?(status, migration)
19
+ end
20
+ end
21
+
22
+ sort_migrations_desc(migrations)
23
+ end
24
+
25
+ def all
26
+ migrations = []
27
+
28
+ MigrationContext.instance.each do |context|
29
+ indexed_migrations = context.migrations.index_by { |m| m.version.to_s }
30
+
31
+ context.migrations_status.each do |status, version|
32
+ migration = indexed_migrations[version]
33
+ migrations << build_migration_struct(status, migration) if should_include?(status, migration)
34
+ end
35
+ end
36
+
37
+ sort_migrations_desc(migrations)
38
+ end
39
+
40
+ def find(version, database)
41
+ MigrationContext.instance.each do |context|
42
+ next unless ActualDbSchema.db_config[:database] == database
43
+
44
+ migration = find_migration_in_context(context, version)
45
+ return migration if migration
46
+ end
47
+ nil
48
+ end
49
+
50
+ def rollback(version, database)
51
+ MigrationContext.instance.each do |context|
52
+ next unless ActualDbSchema.db_config[:database] == database
53
+
54
+ if context.migrations.detect { |m| m.version.to_s == version }
55
+ context.run(:down, version.to_i)
56
+ break
57
+ end
58
+ end
59
+ end
60
+
61
+ def rollback_all
62
+ MigrationContext.instance.each(&:rollback_branches)
63
+ end
64
+
65
+ def migrate(version, database)
66
+ MigrationContext.instance.each do |context|
67
+ next unless ActualDbSchema.db_config[:database] == database
68
+
69
+ if context.migrations.detect { |m| m.version.to_s == version }
70
+ context.run(:up, version.to_i)
71
+ break
72
+ end
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ def build_migration_struct(status, migration)
79
+ Migration.new(
80
+ status: status,
81
+ version: migration.version.to_s,
82
+ name: migration.name,
83
+ branch: branch_for(migration.version),
84
+ database: ActualDbSchema.db_config[:database],
85
+ filename: migration.filename,
86
+ phantom: phantom?(migration)
87
+ )
88
+ end
89
+
90
+ def sort_migrations_desc(migrations)
91
+ migrations.sort_by { |migration| migration[:version].to_i }.reverse if migrations.any?
92
+ end
93
+
94
+ def phantom?(migration)
95
+ migration.filename.include?("/tmp/migrated")
96
+ end
97
+
98
+ def should_include?(status, migration)
99
+ migration && (status == "up" || !phantom?(migration))
100
+ end
101
+
102
+ def find_migration_in_context(context, version)
103
+ migration = context.migrations.detect { |m| m.version.to_s == version }
104
+ return unless migration
105
+
106
+ status = context.migrations_status.detect { |_s, v| v.to_s == version }&.first || "unknown"
107
+ build_migration_struct(status, migration)
108
+ end
109
+
110
+ def branch_for(version)
111
+ metadata.fetch(version.to_s, {})[:branch] || "unknown"
112
+ end
113
+
114
+ def metadata
115
+ @metadata ||= {}
116
+ @metadata[ActualDbSchema.db_config[:database]] ||= ActualDbSchema::Store.instance.read
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActualDbSchema
4
+ # The class manages connections to each database and provides the appropriate migration context for each connection.
5
+ class MigrationContext
6
+ include Singleton
7
+
8
+ def each
9
+ configs.each do |db_config|
10
+ establish_connection(db_config)
11
+ yield context
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def establish_connection(db_config)
18
+ config = db_config.respond_to?(:config) ? db_config.config : db_config
19
+ ActiveRecord::Base.establish_connection(config)
20
+ end
21
+
22
+ def configs
23
+ ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env)
24
+ end
25
+
26
+ def context
27
+ ar_version = Gem::Version.new(ActiveRecord::VERSION::STRING)
28
+ context = if ar_version >= Gem::Version.new("7.2.0") ||
29
+ (ar_version >= Gem::Version.new("7.1.0") && ar_version.prerelease?)
30
+ ActiveRecord::Base.connection_pool.migration_context
31
+ else
32
+ ActiveRecord::Base.connection.migration_context
33
+ end
34
+ context.extend(ActualDbSchema::Patches::MigrationContext)
35
+ end
36
+ end
37
+ end
@@ -5,7 +5,7 @@ module ActualDbSchema
5
5
  # Add new command to roll back the phantom migrations
6
6
  module MigrationContext
7
7
  def rollback_branches(manual_mode: false)
8
- migrations.reverse_each do |migration|
8
+ phantom_migrations.reverse_each do |migration|
9
9
  next unless status_up?(migration)
10
10
 
11
11
  show_info_for(migration) if manual_mode
@@ -15,6 +15,16 @@ module ActualDbSchema
15
15
  end
16
16
  end
17
17
 
18
+ def phantom_migrations
19
+ paths = Array(migrations_paths)
20
+ current_branch_files = Dir[*paths.flat_map { |path| "#{path}/**/[0-9]*_*.rb" }]
21
+ current_branch_file_names = current_branch_files.map { |f| ActualDbSchema.migration_filename(f) }
22
+
23
+ migrations.reject do |migration|
24
+ current_branch_file_names.include?(ActualDbSchema.migration_filename(migration.filename))
25
+ end
26
+ end
27
+
18
28
  private
19
29
 
20
30
  def down_migrator_for(migration)
@@ -31,9 +41,13 @@ module ActualDbSchema
31
41
  paths = Array(migrations_paths)
32
42
  current_branch_files = Dir[*paths.flat_map { |path| "#{path}/**/[0-9]*_*.rb" }]
33
43
  other_branches_files = Dir["#{ActualDbSchema.migrated_folder}/**/[0-9]*_*.rb"]
44
+ current_branch_versions = current_branch_files.map { |file| file.match(/(\d+)_/)[1] }
45
+ filtered_other_branches_files = other_branches_files.reject do |file|
46
+ version = file.match(/(\d+)_/)[1]
47
+ current_branch_versions.include?(version)
48
+ end
34
49
 
35
- current_branch_file_names = current_branch_files.map { |f| ActualDbSchema.migration_filename(f) }
36
- other_branches_files.reject { |f| ActualDbSchema.migration_filename(f).in?(current_branch_file_names) }
50
+ current_branch_files + filtered_other_branches_files
37
51
  end
38
52
 
39
53
  def status_up?(migration)
@@ -52,7 +66,7 @@ module ActualDbSchema
52
66
  puts "\n[ActualDbSchema] A phantom migration was found and is about to be rolled back."
53
67
  puts "Please make a decision from the options below to proceed.\n\n"
54
68
  puts "Branch: #{branch_for(migration.version.to_s)}"
55
- puts "Database: #{db_config[:database]}"
69
+ puts "Database: #{ActualDbSchema.db_config[:database]}"
56
70
  puts "Version: #{migration.version}\n\n"
57
71
  puts File.read(migration.filename)
58
72
  end
@@ -69,14 +83,6 @@ module ActualDbSchema
69
83
  migrator.migrate
70
84
  end
71
85
 
72
- def db_config
73
- @db_config ||= if ActiveRecord::Base.respond_to?(:connection_db_config)
74
- ActiveRecord::Base.connection_db_config.configuration_hash
75
- else
76
- ActiveRecord::Base.connection_config
77
- end
78
- end
79
-
80
86
  def branch_for(version)
81
87
  metadata.fetch(version, {})[:branch] || "unknown"
82
88
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActualDbSchema
4
- VERSION = "0.7.5"
4
+ VERSION = "0.7.6"
5
5
  end
@@ -1,10 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "actual_db_schema/engine"
3
4
  require "active_record/migration"
4
5
  require "csv"
5
6
  require_relative "actual_db_schema/git"
6
7
  require_relative "actual_db_schema/store"
7
8
  require_relative "actual_db_schema/version"
9
+ require_relative "actual_db_schema/migration"
10
+ require_relative "actual_db_schema/migration_context"
8
11
  require_relative "actual_db_schema/patches/migration_proxy"
9
12
  require_relative "actual_db_schema/patches/migrator"
10
13
  require_relative "actual_db_schema/patches/migration_context"
@@ -17,8 +20,6 @@ require_relative "actual_db_schema/commands/list"
17
20
  module ActualDbSchema
18
21
  raise NotImplementedError, "ActualDbSchema is only supported in Rails" unless defined?(Rails)
19
22
 
20
- require "railtie"
21
-
22
23
  class << self
23
24
  attr_accessor :config, :failed
24
25
  end
@@ -26,7 +27,8 @@ module ActualDbSchema
26
27
  self.failed = []
27
28
  self.config = {
28
29
  enabled: Rails.env.development?,
29
- auto_rollback_disabled: ENV["ACTUAL_DB_SCHEMA_AUTO_ROLLBACK_DISABLED"].present?
30
+ auto_rollback_disabled: ENV["ACTUAL_DB_SCHEMA_AUTO_ROLLBACK_DISABLED"].present?,
31
+ ui_enabled: Rails.env.development? || ENV["ACTUAL_DB_SCHEMA_UI_ENABLED"].present?
30
32
  }
31
33
 
32
34
  def self.migrated_folder
@@ -58,17 +60,16 @@ module ActualDbSchema
58
60
  end
59
61
  end
60
62
 
61
- def self.migration_filename(fullpath)
62
- fullpath.split("/").last
63
+ def self.db_config
64
+ if ActiveRecord::Base.respond_to?(:connection_db_config)
65
+ ActiveRecord::Base.connection_db_config.configuration_hash
66
+ else
67
+ ActiveRecord::Base.connection_config
68
+ end
63
69
  end
64
70
 
65
- def self.for_each_db_connection
66
- configs = ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env)
67
- configs.each do |db_config|
68
- config = db_config.respond_to?(:config) ? db_config.config : db_config
69
- ActiveRecord::Base.establish_connection(config)
70
- yield
71
- end
71
+ def self.migration_filename(fullpath)
72
+ fullpath.split("/").last
72
73
  end
73
74
  end
74
75
 
data/lib/tasks/db.rake CHANGED
@@ -4,8 +4,8 @@ namespace :db do
4
4
  desc "Rollback migrations that were run inside not a merged branch."
5
5
  task rollback_branches: :load_config do
6
6
  ActualDbSchema.failed = []
7
- ActualDbSchema.for_each_db_connection do
8
- ActualDbSchema::Commands::Rollback.new.call
7
+ ActualDbSchema::MigrationContext.instance.each do |context|
8
+ ActualDbSchema::Commands::Rollback.new(context).call
9
9
  end
10
10
  end
11
11
 
@@ -13,16 +13,16 @@ namespace :db do
13
13
  desc "Manually rollback phantom migrations one by one"
14
14
  task manual: :load_config do
15
15
  ActualDbSchema.failed = []
16
- ActualDbSchema.for_each_db_connection do
17
- ActualDbSchema::Commands::Rollback.new(manual_mode: true).call
16
+ ActualDbSchema::MigrationContext.instance.each do |context|
17
+ ActualDbSchema::Commands::Rollback.new(context, manual_mode: true).call
18
18
  end
19
19
  end
20
20
  end
21
21
 
22
22
  desc "List all phantom migrations - non-relevant migrations that were run inside not a merged branch."
23
23
  task phantom_migrations: :load_config do
24
- ActualDbSchema.for_each_db_connection do
25
- ActualDbSchema::Commands::List.new.call
24
+ ActualDbSchema::MigrationContext.instance.each do |context|
25
+ ActualDbSchema::Commands::List.new(context).call
26
26
  end
27
27
  end
28
28
 
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.7.5
4
+ version: 0.7.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Kaleshka
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-06-24 00:00:00.000000000 Z
11
+ date: 2024-07-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -127,6 +127,15 @@ files:
127
127
  - README.md
128
128
  - Rakefile
129
129
  - actual_db_schema.gemspec
130
+ - app/assets/javascripts/application.js
131
+ - app/assets/stylesheets/styles.css
132
+ - app/controllers/actual_db_schema/migrations_controller.rb
133
+ - app/controllers/actual_db_schema/phantom_migrations_controller.rb
134
+ - app/views/actual_db_schema/migrations/index.html.erb
135
+ - app/views/actual_db_schema/migrations/show.html.erb
136
+ - app/views/actual_db_schema/phantom_migrations/index.html.erb
137
+ - app/views/actual_db_schema/phantom_migrations/show.html.erb
138
+ - config/routes.rb
130
139
  - gemfiles/rails.6.0.gemfile
131
140
  - gemfiles/rails.6.1.gemfile
132
141
  - gemfiles/rails.7.0.gemfile
@@ -136,13 +145,15 @@ files:
136
145
  - lib/actual_db_schema/commands/base.rb
137
146
  - lib/actual_db_schema/commands/list.rb
138
147
  - lib/actual_db_schema/commands/rollback.rb
148
+ - lib/actual_db_schema/engine.rb
139
149
  - lib/actual_db_schema/git.rb
150
+ - lib/actual_db_schema/migration.rb
151
+ - lib/actual_db_schema/migration_context.rb
140
152
  - lib/actual_db_schema/patches/migration_context.rb
141
153
  - lib/actual_db_schema/patches/migration_proxy.rb
142
154
  - lib/actual_db_schema/patches/migrator.rb
143
155
  - lib/actual_db_schema/store.rb
144
156
  - lib/actual_db_schema/version.rb
145
- - lib/railtie.rb
146
157
  - lib/tasks/db.rake
147
158
  - sig/actual_db_schema.rbs
148
159
  homepage: https://blog.widefix.com/actual-db-schema/
data/lib/railtie.rb DELETED
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rails"
4
-
5
- module ActualDbSchema
6
- # Load the task into Rails app
7
- class Railtie < Rails::Railtie
8
- railtie_name :actual_db_schema
9
-
10
- rake_tasks do
11
- path = File.expand_path(__dir__)
12
- Dir.glob("#{path}/tasks/**/*.rake").each { |f| load f }
13
- end
14
- end
15
- end