actual_db_schema 0.8.2 → 0.8.3
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 +7 -0
- data/Gemfile.lock +1 -1
- data/README.md +64 -1
- data/app/controllers/actual_db_schema/broken_versions_controller.rb +41 -0
- data/app/controllers/actual_db_schema/migrations_controller.rb +14 -0
- data/app/controllers/actual_db_schema/schema_controller.rb +18 -0
- data/app/views/actual_db_schema/broken_versions/index.html.erb +64 -0
- data/app/views/actual_db_schema/migrations/index.html.erb +62 -50
- data/app/views/actual_db_schema/schema/index.html.erb +31 -0
- data/app/views/actual_db_schema/shared/_js.html +6 -0
- data/app/views/actual_db_schema/shared/_style.html +45 -0
- data/config/routes.rb +10 -0
- data/lib/actual_db_schema/configuration.rb +2 -1
- data/lib/actual_db_schema/migration.rb +49 -1
- data/lib/actual_db_schema/schema_diff.rb +12 -4
- data/lib/actual_db_schema/schema_diff_html.rb +122 -0
- data/lib/actual_db_schema/version.rb +1 -1
- data/lib/actual_db_schema.rb +2 -1
- data/lib/generators/actual_db_schema/templates/actual_db_schema.rb +5 -1
- data/lib/tasks/actual_db_schema.rake +26 -2
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d78699aee32920e20a6dd084512949d9cc88e7878531de087295d43833463193
|
4
|
+
data.tar.gz: 1e55c7f7b15e0a2fbace58cc80c32deb2061bbe6cd8d8da69f8f35682e9d5b75
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: df1251d4d45888438f9e5ab3585eafa2f2eec2ace597ec0b6985479dc835a67460990e5b6e67534bfeceb779f088b64c65fcff0c9a7b7e16007a90c365f7d93e
|
7
|
+
data.tar.gz: 94a00404bb421f3b3a1124c456358003da71762a652cca2d6e89763bdbc872ddf3f86c2c68ca9b5cc26e12a514869f9c11aba972dfb705f816dc3d43200e63a1
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
## [0.8.3] - 2025-03-03
|
2
|
+
|
3
|
+
- View Schema with Migration Annotations in the UI
|
4
|
+
- Clean Up Broken Migrations
|
5
|
+
- Filter Migrations in the UI
|
6
|
+
- Customize Your Migrated Folder Location
|
7
|
+
|
1
8
|
## [0.8.2] - 2025-02-06
|
2
9
|
|
3
10
|
- Show migration name in the schema.rb diff that caused the change
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -72,6 +72,25 @@ The gem offers the following rake tasks that can be manually run according to yo
|
|
72
72
|
- `rails db:rollback_branches:manual` - run it to manually rolls back phantom migrations one by one.
|
73
73
|
- `rails db:phantom_migrations` - displays a list of phantom migrations.
|
74
74
|
|
75
|
+
## Migrated Folder Configuration
|
76
|
+
|
77
|
+
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
|
+
|
79
|
+
### 1. Using Environment Variable
|
80
|
+
|
81
|
+
Set the environment variable `ACTUAL_DB_SCHEMA_MIGRATED_FOLDER` to your desired folder path:
|
82
|
+
|
83
|
+
```sh
|
84
|
+
export ACTUAL_DB_SCHEMA_MIGRATED_FOLDER="custom/migrated"
|
85
|
+
```
|
86
|
+
|
87
|
+
### 2. Using Initializer
|
88
|
+
Add the following line to your initializer file (`config/initializers/actual_db_schema.rb`):
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
config.migrated_folder = Rails.root.join("custom", "migrated")
|
92
|
+
```
|
93
|
+
|
75
94
|
## Accessing the UI
|
76
95
|
|
77
96
|
The UI for managing migrations is enabled automatically. To access the UI, simply navigate to the following URL in your web browser:
|
@@ -228,6 +247,49 @@ remove_index :users, :email
|
|
228
247
|
rename_column :users, :username, :handle
|
229
248
|
```
|
230
249
|
|
250
|
+
## Delete Broken Migrations
|
251
|
+
|
252
|
+
A migration is considered broken if it has been migrated in the database but the corresponding migration file is missing. This functionality allows you to safely delete these broken versions from the database to keep it clean.
|
253
|
+
|
254
|
+
You can delete broken migrations using either of the following methods:
|
255
|
+
|
256
|
+
### 1. Using the UI
|
257
|
+
|
258
|
+
Navigate to the following URL in your web browser:
|
259
|
+
```
|
260
|
+
http://localhost:3000/rails/broken_versions
|
261
|
+
```
|
262
|
+
|
263
|
+
This page lists all broken versions and provides an option to delete them.
|
264
|
+
|
265
|
+
### 2. Using a Rake Task
|
266
|
+
|
267
|
+
To delete all broken migrations, run:
|
268
|
+
```sh
|
269
|
+
rake actual_db_schema:delete_broken_versions
|
270
|
+
```
|
271
|
+
|
272
|
+
To delete specific migrations, pass the migration version(s) and optionally a database:
|
273
|
+
```sh
|
274
|
+
rake actual_db_schema:delete_broken_versions[<version>, <version>]
|
275
|
+
```
|
276
|
+
|
277
|
+
- `<version>` – The migration version(s) to delete (space-separated if multiple).
|
278
|
+
- `<database>` (optional) – Specify a database if using multiple databases.
|
279
|
+
|
280
|
+
#### Examples:
|
281
|
+
|
282
|
+
```sh
|
283
|
+
# Delete all broken migrations
|
284
|
+
rake actual_db_schema:delete_broken_versions
|
285
|
+
|
286
|
+
# Delete specific migrations
|
287
|
+
rake actual_db_schema:delete_broken_versions["20250224103352 20250224103358"]
|
288
|
+
|
289
|
+
# Delete specific migrations from a specific database
|
290
|
+
rake actual_db_schema:delete_broken_versions["20250224103352 20250224103358", "primary"]
|
291
|
+
```
|
292
|
+
|
231
293
|
## Development
|
232
294
|
|
233
295
|
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.
|
@@ -241,7 +303,8 @@ To release a new version do the following in the order:
|
|
241
303
|
- `bundle install` to update `Gemfile.lock`;
|
242
304
|
- make the commit and push;
|
243
305
|
- run `bundle exec rake release`. This will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org);
|
244
|
-
- [announce the new release on GitHub](https://github.com/widefix/actual_db_schema/releases)
|
306
|
+
- [announce the new release on GitHub](https://github.com/widefix/actual_db_schema/releases);
|
307
|
+
- close the milestone on GitHub.
|
245
308
|
|
246
309
|
### Running Tests with Specific Rails Versions
|
247
310
|
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActualDbSchema
|
4
|
+
# Controller for managing broken migration versions.
|
5
|
+
class BrokenVersionsController < ActionController::Base
|
6
|
+
protect_from_forgery with: :exception
|
7
|
+
skip_before_action :verify_authenticity_token
|
8
|
+
|
9
|
+
def index; end
|
10
|
+
|
11
|
+
def delete
|
12
|
+
handle_delete(params[:id], params[:database])
|
13
|
+
redirect_to broken_versions_path
|
14
|
+
end
|
15
|
+
|
16
|
+
def delete_all
|
17
|
+
handle_delete_all
|
18
|
+
redirect_to broken_versions_path
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def handle_delete(id, database)
|
24
|
+
ActualDbSchema::Migration.instance.delete(id, database)
|
25
|
+
flash[:notice] = "Migration #{id} was successfully deleted."
|
26
|
+
rescue StandardError => e
|
27
|
+
flash[:alert] = e.message
|
28
|
+
end
|
29
|
+
|
30
|
+
def handle_delete_all
|
31
|
+
ActualDbSchema::Migration.instance.delete_all
|
32
|
+
flash[:notice] = "All broken versions were successfully deleted."
|
33
|
+
rescue StandardError => e
|
34
|
+
flash[:alert] = e.message
|
35
|
+
end
|
36
|
+
|
37
|
+
helper_method def broken_versions
|
38
|
+
@broken_versions ||= ActualDbSchema::Migration.instance.broken_versions
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -40,6 +40,20 @@ module ActualDbSchema
|
|
40
40
|
|
41
41
|
helper_method def migrations
|
42
42
|
@migrations ||= ActualDbSchema::Migration.instance.all
|
43
|
+
query = params[:query].to_s.strip.downcase
|
44
|
+
|
45
|
+
return @migrations if query.blank?
|
46
|
+
|
47
|
+
@migrations.select do |migration|
|
48
|
+
file_name_matches = migration[:filename].include?(query)
|
49
|
+
content_matches = begin
|
50
|
+
File.read(migration[:filename]).downcase.include?(query)
|
51
|
+
rescue StandardError
|
52
|
+
false
|
53
|
+
end
|
54
|
+
|
55
|
+
file_name_matches || content_matches
|
56
|
+
end
|
43
57
|
end
|
44
58
|
|
45
59
|
helper_method def migration
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActualDbSchema
|
4
|
+
# Controller to display the database schema diff.
|
5
|
+
class SchemaController < ActionController::Base
|
6
|
+
protect_from_forgery with: :exception
|
7
|
+
skip_before_action :verify_authenticity_token
|
8
|
+
|
9
|
+
def index; end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
helper_method def schema_diff_html
|
14
|
+
schema_diff = ActualDbSchema::SchemaDiffHtml.new("./db/schema.rb", "db/migrate")
|
15
|
+
schema_diff.render_html(params[:table])
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Broken Versions</title>
|
5
|
+
<%= render partial: 'actual_db_schema/shared/js' %>
|
6
|
+
<%= render partial: 'actual_db_schema/shared/style' %>
|
7
|
+
</head>
|
8
|
+
<body>
|
9
|
+
<div>
|
10
|
+
<% flash.each do |key, message| %>
|
11
|
+
<div class="flash <%= key %>"><%= message %></div>
|
12
|
+
<% end %>
|
13
|
+
<h2>Broken Versions</h2>
|
14
|
+
<p>
|
15
|
+
These are versions that were migrated in the database, but the corresponding migration file is missing.
|
16
|
+
You can safely delete them from the database to clean up.
|
17
|
+
</p>
|
18
|
+
<div class="top-buttons">
|
19
|
+
<%= link_to 'All Migrations', migrations_path, class: "top-button" %>
|
20
|
+
<% if broken_versions.present? %>
|
21
|
+
<%= button_to '✖ Delete all',
|
22
|
+
delete_all_broken_versions_path,
|
23
|
+
method: :post,
|
24
|
+
data: { confirm: 'These migrations do not have corresponding migration files. Proceeding will remove these entries from the `schema_migrations` table. Are you sure you want to continue?' },
|
25
|
+
class: 'button migration-action' %>
|
26
|
+
<% end %>
|
27
|
+
</div>
|
28
|
+
<% if broken_versions.present? %>
|
29
|
+
<table>
|
30
|
+
<thead>
|
31
|
+
<tr>
|
32
|
+
<th>Status</th>
|
33
|
+
<th>Migration ID</th>
|
34
|
+
<th>Branch</th>
|
35
|
+
<th>Database</th>
|
36
|
+
<th>Actions</th>
|
37
|
+
</tr>
|
38
|
+
</thead>
|
39
|
+
<tbody>
|
40
|
+
<% broken_versions.each do |version| %>
|
41
|
+
<tr class="migration-row phantom">
|
42
|
+
<td><%= version[:status] %></td>
|
43
|
+
<td><%= version[:version] %></td>
|
44
|
+
<td><%= version[:branch] %></td>
|
45
|
+
<td><%= version[:database] %></td>
|
46
|
+
<td>
|
47
|
+
<div class='button-container'>
|
48
|
+
<%= button_to '✖ Delete',
|
49
|
+
delete_broken_version_path(id: version[:version], database: version[:database]),
|
50
|
+
method: :post,
|
51
|
+
data: { confirm: 'This migration does not have a corresponding migration file. Proceeding will remove its entry from the `schema_migrations` table. Are you sure you want to continue?' },
|
52
|
+
class: 'button migration-action' %>
|
53
|
+
</div>
|
54
|
+
</td>
|
55
|
+
</tr>
|
56
|
+
<% end %>
|
57
|
+
</tbody>
|
58
|
+
</table>
|
59
|
+
<% else %>
|
60
|
+
<p>No broken versions found.</p>
|
61
|
+
<% end %>
|
62
|
+
</div>
|
63
|
+
</body>
|
64
|
+
</html>
|
@@ -14,57 +14,69 @@
|
|
14
14
|
<p>
|
15
15
|
<span style="background-color: #ffe6e6; padding: 0 5px;">Red rows</span> represent phantom migrations.
|
16
16
|
</p>
|
17
|
-
<div class="
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
<
|
27
|
-
|
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>
|
17
|
+
<div class="container">
|
18
|
+
<div class="top-controls">
|
19
|
+
<div class="top-buttons">
|
20
|
+
<%= link_to 'Phantom Migrations', phantom_migrations_path, class: "top-button" %>
|
21
|
+
<%= link_to 'Broken Versions', broken_versions_path, class: "top-button" %>
|
22
|
+
<%= link_to 'View Schema', schema_path, class: "top-button" %>
|
23
|
+
</div>
|
24
|
+
<div class="top-search">
|
25
|
+
<%= form_tag migrations_path, method: :get, class: "search-form" do %>
|
26
|
+
<span class="search-icon">🔍</span>
|
27
|
+
<%= text_field_tag :query, params[:query], placeholder: "Search migrations by name or content", class: "search-input" %>
|
62
28
|
<% end %>
|
63
|
-
</
|
64
|
-
</
|
65
|
-
|
66
|
-
|
67
|
-
|
29
|
+
</div>
|
30
|
+
</div>
|
31
|
+
<% if migrations.present? %>
|
32
|
+
<table>
|
33
|
+
<thead>
|
34
|
+
<tr>
|
35
|
+
<th>Status</th>
|
36
|
+
<th>Migration ID</th>
|
37
|
+
<th>Name</th>
|
38
|
+
<th>Branch</th>
|
39
|
+
<th>Database</th>
|
40
|
+
<th>Actions</th>
|
41
|
+
</tr>
|
42
|
+
</thead>
|
43
|
+
<tbody>
|
44
|
+
<% migrations.each do |migration| %>
|
45
|
+
<tr class="migration-row <%= migration[:phantom] ? 'phantom' : 'normal' %>">
|
46
|
+
<td><%= migration[:status] %></td>
|
47
|
+
<td><%= migration[:version] %></td>
|
48
|
+
<td>
|
49
|
+
<div class="truncate-text" title="<%= migration[:name] %>">
|
50
|
+
<%= migration[:name] %>
|
51
|
+
</div>
|
52
|
+
</td>
|
53
|
+
<td><%= migration[:branch] %></td>
|
54
|
+
<td><%= migration[:database] %></td>
|
55
|
+
<td>
|
56
|
+
<div class='button-container'>
|
57
|
+
<%= link_to '👁 Show',
|
58
|
+
migration_path(id: migration[:version], database: migration[:database]),
|
59
|
+
class: 'button' %>
|
60
|
+
<%= button_to '⎌ Rollback',
|
61
|
+
rollback_migration_path(id: migration[:version], database: migration[:database]),
|
62
|
+
method: :post,
|
63
|
+
class: 'button migration-action',
|
64
|
+
style: ('display: none;' if migration[:status] == "down") %>
|
65
|
+
<%= button_to '⬆ Migrate',
|
66
|
+
migrate_migration_path(id: migration[:version], database: migration[:database]),
|
67
|
+
method: :post,
|
68
|
+
class: 'button migration-action',
|
69
|
+
style: ('display: none;' if migration[:status] == "up" || migration[:phantom]) %>
|
70
|
+
</div>
|
71
|
+
</td>
|
72
|
+
</tr>
|
73
|
+
<% end %>
|
74
|
+
</tbody>
|
75
|
+
</table>
|
76
|
+
<% else %>
|
77
|
+
<p>No migrations found.</p>
|
78
|
+
<% end %>
|
79
|
+
</div>
|
68
80
|
</div>
|
69
81
|
</body>
|
70
82
|
</html>
|
@@ -0,0 +1,31 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Database Schema</title>
|
5
|
+
<%= render partial: 'actual_db_schema/shared/js' %>
|
6
|
+
<%= render partial: 'actual_db_schema/shared/style' %>
|
7
|
+
</head>
|
8
|
+
<body>
|
9
|
+
<div>
|
10
|
+
<% flash.each do |key, message| %>
|
11
|
+
<div class="flash <%= key %>"><%= message %></div>
|
12
|
+
<% end %>
|
13
|
+
<h2>Database Schema</h2>
|
14
|
+
<div class="top-controls">
|
15
|
+
<div class="top-buttons">
|
16
|
+
<%= link_to 'All Migrations', migrations_path, class: "top-button" %>
|
17
|
+
</div>
|
18
|
+
<div class="top-search">
|
19
|
+
<%= form_tag schema_path, method: :get, class: "search-form" do %>
|
20
|
+
<span class="search-icon">🔍</span>
|
21
|
+
<%= text_field_tag :table, params[:table], placeholder: "Filter by table name", class: "search-input" %>
|
22
|
+
<% end %>
|
23
|
+
</div>
|
24
|
+
</div>
|
25
|
+
|
26
|
+
<div class="schema-diff">
|
27
|
+
<pre><%= raw schema_diff_html %></pre>
|
28
|
+
</div>
|
29
|
+
</div>
|
30
|
+
</body>
|
31
|
+
</html>
|
@@ -4,6 +4,12 @@
|
|
4
4
|
|
5
5
|
migrationActions.forEach(button => {
|
6
6
|
button.addEventListener('click', function(event) {
|
7
|
+
const confirmMessage = button.dataset.confirm;
|
8
|
+
if (confirmMessage && !confirm(confirmMessage)) {
|
9
|
+
event.preventDefault();
|
10
|
+
return;
|
11
|
+
}
|
12
|
+
|
7
13
|
const originalText = button.value;
|
8
14
|
button.value = 'Loading...';
|
9
15
|
disableButtons();
|
@@ -15,6 +15,10 @@
|
|
15
15
|
padding-left: 10px;
|
16
16
|
}
|
17
17
|
|
18
|
+
p {
|
19
|
+
padding-left: 10px;
|
20
|
+
}
|
21
|
+
|
18
22
|
table {
|
19
23
|
margin: 0;
|
20
24
|
border-collapse: collapse;
|
@@ -65,6 +69,7 @@
|
|
65
69
|
text-decoration: none;
|
66
70
|
display: inline-block;
|
67
71
|
margin: 0 2px;
|
72
|
+
margin-right: 8px;
|
68
73
|
cursor: pointer;
|
69
74
|
border-radius: 4px;
|
70
75
|
transition: background-color 0.3s;
|
@@ -113,4 +118,44 @@
|
|
113
118
|
background-color: #f8d7da;
|
114
119
|
color: #721c24;
|
115
120
|
}
|
121
|
+
|
122
|
+
.container {
|
123
|
+
display: inline-block;
|
124
|
+
max-width: 100%;
|
125
|
+
}
|
126
|
+
|
127
|
+
.top-controls {
|
128
|
+
display: flex;
|
129
|
+
justify-content: space-between;
|
130
|
+
align-items: center;
|
131
|
+
width: 100%;
|
132
|
+
}
|
133
|
+
|
134
|
+
.top-search {
|
135
|
+
display: flex;
|
136
|
+
align-items: center;
|
137
|
+
justify-content: flex-end;
|
138
|
+
}
|
139
|
+
|
140
|
+
.search-form {
|
141
|
+
display: flex;
|
142
|
+
align-items: center;
|
143
|
+
}
|
144
|
+
|
145
|
+
.search-form .search-icon {
|
146
|
+
margin-right: 5px;
|
147
|
+
font-size: 16px;
|
148
|
+
}
|
149
|
+
|
150
|
+
.search-form .search-input {
|
151
|
+
padding: 5px;
|
152
|
+
border: 1px solid #ccc;
|
153
|
+
border-radius: 4px;
|
154
|
+
font-size: 13px;
|
155
|
+
width: 250px;
|
156
|
+
}
|
157
|
+
|
158
|
+
.schema-diff {
|
159
|
+
margin-left: 8px;
|
160
|
+
}
|
116
161
|
</style>
|
data/config/routes.rb
CHANGED
@@ -15,4 +15,14 @@ ActualDbSchema::Engine.routes.draw do
|
|
15
15
|
post :rollback_all
|
16
16
|
end
|
17
17
|
end
|
18
|
+
resources :broken_versions, only: %i[index] do
|
19
|
+
member do
|
20
|
+
post :delete
|
21
|
+
end
|
22
|
+
collection do
|
23
|
+
post :delete_all
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
get "schema", to: "schema#index", as: :schema
|
18
28
|
end
|
@@ -4,7 +4,7 @@ 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
|
7
|
+
:console_migrations_enabled, :migrated_folder
|
8
8
|
|
9
9
|
def initialize
|
10
10
|
@enabled = Rails.env.development?
|
@@ -13,6 +13,7 @@ module ActualDbSchema
|
|
13
13
|
@git_hooks_enabled = ENV["ACTUAL_DB_SCHEMA_GIT_HOOKS_ENABLED"].present?
|
14
14
|
@multi_tenant_schemas = nil
|
15
15
|
@console_migrations_enabled = ENV["ACTUAL_DB_SCHEMA_CONSOLE_MIGRATIONS_ENABLED"].present?
|
16
|
+
@migrated_folder = ENV["ACTUAL_DB_SCHEMA_MIGRATED_FOLDER"].present?
|
16
17
|
end
|
17
18
|
|
18
19
|
def [](key)
|
@@ -73,6 +73,43 @@ module ActualDbSchema
|
|
73
73
|
end
|
74
74
|
end
|
75
75
|
|
76
|
+
def broken_versions
|
77
|
+
broken = []
|
78
|
+
MigrationContext.instance.each do |context|
|
79
|
+
context.migrations_status.each do |status, version, name|
|
80
|
+
next unless name == "********** NO FILE **********"
|
81
|
+
|
82
|
+
broken << Migration.new(
|
83
|
+
status: status,
|
84
|
+
version: version.to_s,
|
85
|
+
name: name,
|
86
|
+
branch: branch_for(version),
|
87
|
+
database: ActualDbSchema.db_config[:database]
|
88
|
+
)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
broken
|
93
|
+
end
|
94
|
+
|
95
|
+
def delete(version, database)
|
96
|
+
validate_broken_migration(version, database)
|
97
|
+
|
98
|
+
MigrationContext.instance.each do
|
99
|
+
next if database && ActualDbSchema.db_config[:database] != database
|
100
|
+
next if ActiveRecord::Base.connection.select_values("SELECT version FROM schema_migrations").exclude?(version)
|
101
|
+
|
102
|
+
ActiveRecord::Base.connection.execute("DELETE FROM schema_migrations WHERE version = '#{version}'")
|
103
|
+
break
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def delete_all
|
108
|
+
broken_versions.each do |version|
|
109
|
+
delete(version.version, version.database)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
76
113
|
private
|
77
114
|
|
78
115
|
def build_migration_struct(status, migration)
|
@@ -92,7 +129,8 @@ module ActualDbSchema
|
|
92
129
|
end
|
93
130
|
|
94
131
|
def phantom?(migration)
|
95
|
-
|
132
|
+
migrated_folder = ActualDbSchema.config[:migrated_folder].presence || "/tmp/migrated"
|
133
|
+
migration.filename.include?(migrated_folder.to_s)
|
96
134
|
end
|
97
135
|
|
98
136
|
def should_include?(status, migration)
|
@@ -115,5 +153,15 @@ module ActualDbSchema
|
|
115
153
|
@metadata ||= {}
|
116
154
|
@metadata[ActualDbSchema.db_config[:database]] ||= ActualDbSchema::Store.instance.read
|
117
155
|
end
|
156
|
+
|
157
|
+
def validate_broken_migration(version, database)
|
158
|
+
if database
|
159
|
+
unless broken_versions.any? { |v| v.version == version && v.database == database }
|
160
|
+
raise StandardError, "Migration is not broken for database #{database}."
|
161
|
+
end
|
162
|
+
else
|
163
|
+
raise StandardError, "Migration is not broken." unless broken_versions.any? { |v| v.version == version }
|
164
|
+
end
|
165
|
+
end
|
118
166
|
end
|
119
167
|
end
|
@@ -63,17 +63,25 @@ module ActualDbSchema
|
|
63
63
|
end
|
64
64
|
|
65
65
|
def migrated_folders
|
66
|
+
dirs = find_migrated_folders
|
67
|
+
|
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
|
72
|
+
|
73
|
+
dirs.map { |dir| dir.sub(%r{\A\./}, "") }.uniq
|
74
|
+
end
|
75
|
+
|
76
|
+
def find_migrated_folders
|
66
77
|
path_parts = Pathname.new(@migrations_path).each_filename.to_a
|
67
78
|
db_index = path_parts.index("db")
|
68
|
-
|
69
79
|
return [] unless db_index
|
70
80
|
|
71
81
|
base_path = db_index.zero? ? "." : File.join(*path_parts[0...db_index])
|
72
|
-
|
82
|
+
Dir[File.join(base_path, "tmp", "migrated*")].select do |path|
|
73
83
|
File.directory?(path) && File.basename(path).match?(/^migrated(_[a-zA-Z0-9_-]+)?$/)
|
74
84
|
end
|
75
|
-
|
76
|
-
dirs.map { |dir| dir.sub(%r{\A\./}, "") }
|
77
85
|
end
|
78
86
|
|
79
87
|
def generate_diff(old_content, new_content)
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActualDbSchema
|
4
|
+
# Generates an HTML representation of the schema diff,
|
5
|
+
# annotated with the migrations responsible for each change.
|
6
|
+
class SchemaDiffHtml < SchemaDiff
|
7
|
+
def render_html(table_filter)
|
8
|
+
return unless old_schema_content && !old_schema_content.strip.empty?
|
9
|
+
|
10
|
+
@full_diff_html ||= generate_diff_html
|
11
|
+
filter = table_filter.to_s.strip.downcase
|
12
|
+
|
13
|
+
filter.empty? ? @full_diff_html : extract_table_section(@full_diff_html, filter)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def generate_diff_html
|
19
|
+
diff_output = generate_full_diff(old_schema_content, new_schema_content)
|
20
|
+
return "<pre>#{ERB::Util.html_escape(new_schema_content)}</pre>" if diff_output.strip.empty?
|
21
|
+
|
22
|
+
process_diff_output_for_html(diff_output)
|
23
|
+
end
|
24
|
+
|
25
|
+
def generate_full_diff(old_content, new_content)
|
26
|
+
Tempfile.create("old_schema") do |old_file|
|
27
|
+
Tempfile.create("new_schema") do |new_file|
|
28
|
+
old_file.write(old_content)
|
29
|
+
new_file.write(new_content)
|
30
|
+
old_file.rewind
|
31
|
+
new_file.rewind
|
32
|
+
|
33
|
+
`diff -u -U 9999999 #{old_file.path} #{new_file.path}`
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def process_diff_output_for_html(diff_str)
|
39
|
+
current_table = nil
|
40
|
+
result_lines = []
|
41
|
+
@tables = {}
|
42
|
+
table_start = nil
|
43
|
+
block_depth = 1
|
44
|
+
|
45
|
+
diff_str.lines.each do |line|
|
46
|
+
next if line.start_with?("---") || line.start_with?("+++") || line.match(/^@@/)
|
47
|
+
|
48
|
+
current_table, table_start, block_depth =
|
49
|
+
process_table(line, current_table, table_start, result_lines.size, block_depth)
|
50
|
+
result_lines << (%w[+ -].include?(line[0]) ? handle_diff_line_html(line, current_table) : line)
|
51
|
+
end
|
52
|
+
|
53
|
+
result_lines.join
|
54
|
+
end
|
55
|
+
|
56
|
+
def process_table(line, current_table, table_start, table_end, block_depth)
|
57
|
+
if (ct = line.match(/create_table\s+["']([^"']+)["']/))
|
58
|
+
return [ct[1], table_end, block_depth]
|
59
|
+
end
|
60
|
+
|
61
|
+
return [current_table, table_start, block_depth] unless current_table
|
62
|
+
|
63
|
+
block_depth += line.scan(/\bdo\b/).size unless line.match(/create_table\s+["']([^"']+)["']/)
|
64
|
+
block_depth -= line.scan(/\bend\b/).size
|
65
|
+
|
66
|
+
if block_depth.zero?
|
67
|
+
@tables[current_table] = { start: table_start, end: table_end }
|
68
|
+
current_table = nil
|
69
|
+
block_depth = 1
|
70
|
+
end
|
71
|
+
|
72
|
+
[current_table, table_start, block_depth]
|
73
|
+
end
|
74
|
+
|
75
|
+
def handle_diff_line_html(line, current_table)
|
76
|
+
sign = line[0]
|
77
|
+
line_content = line[1..]
|
78
|
+
color = SIGN_COLORS[sign]
|
79
|
+
|
80
|
+
action, name = detect_action_and_name(line_content, sign, current_table)
|
81
|
+
annotation = action ? find_migrations(action, current_table, name) : []
|
82
|
+
annotation.any? ? annotate_line(line, annotation, color) : colorize_html(line, color)
|
83
|
+
end
|
84
|
+
|
85
|
+
def annotate_line(line, migration_file_paths, color)
|
86
|
+
links_html = migration_file_paths.map { |path| link_to_migration(path) }.join(", ")
|
87
|
+
"#{colorize_html(line.chomp, color)}#{colorize_html(" // #{links_html} //", :gray)}\n"
|
88
|
+
end
|
89
|
+
|
90
|
+
def colorize_html(text, color)
|
91
|
+
safe = ERB::Util.html_escape(text)
|
92
|
+
|
93
|
+
case color
|
94
|
+
when :green
|
95
|
+
%(<span style="color: green">#{safe}</span>)
|
96
|
+
when :red
|
97
|
+
%(<span style="color: red">#{safe}</span>)
|
98
|
+
when :gray
|
99
|
+
%(<span style="color: gray">#{text}</span>)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def link_to_migration(migration_file_path)
|
104
|
+
migration = migrations.detect { |m| m.filename == migration_file_path }
|
105
|
+
return ERB::Util.html_escape(migration_file_path) unless migration
|
106
|
+
|
107
|
+
url = "migrations/#{migration.version}?database=#{migration.database}"
|
108
|
+
"<a href=\"#{url}\">#{ERB::Util.html_escape(migration_file_path)}</a>"
|
109
|
+
end
|
110
|
+
|
111
|
+
def migrations
|
112
|
+
@migrations ||= ActualDbSchema::Migration.instance.all
|
113
|
+
end
|
114
|
+
|
115
|
+
def extract_table_section(full_diff_html, table_name)
|
116
|
+
return unless @tables[table_name]
|
117
|
+
|
118
|
+
range = @tables[table_name]
|
119
|
+
full_diff_html.lines[range[:start]..range[:end]].join
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
data/lib/actual_db_schema.rb
CHANGED
@@ -19,6 +19,7 @@ require_relative "actual_db_schema/git_hooks"
|
|
19
19
|
require_relative "actual_db_schema/multi_tenant"
|
20
20
|
require_relative "actual_db_schema/railtie"
|
21
21
|
require_relative "actual_db_schema/schema_diff"
|
22
|
+
require_relative "actual_db_schema/schema_diff_html"
|
22
23
|
require_relative "actual_db_schema/schema_parser"
|
23
24
|
|
24
25
|
require_relative "actual_db_schema/commands/base"
|
@@ -58,7 +59,7 @@ module ActualDbSchema
|
|
58
59
|
end
|
59
60
|
|
60
61
|
def self.default_migrated_folder
|
61
|
-
Rails.root.join("tmp", "migrated")
|
62
|
+
config[:migrated_folder] || Rails.root.join("tmp", "migrated")
|
62
63
|
end
|
63
64
|
|
64
65
|
def self.migrations_paths
|
@@ -21,7 +21,11 @@ ActualDbSchema.configure do |config|
|
|
21
21
|
# If your application leverages multiple schemas for multi-tenancy, define the active schemas.
|
22
22
|
# config.multi_tenant_schemas = -> { ["public", "tenant1", "tenant2"] }
|
23
23
|
|
24
|
-
# Enable console migrations
|
24
|
+
# Enable console migrations.
|
25
25
|
# config.console_migrations_enabled = true
|
26
26
|
config.console_migrations_enabled = ENV["ACTUAL_DB_SCHEMA_CONSOLE_MIGRATIONS_ENABLED"].present?
|
27
|
+
|
28
|
+
# Define the migrated folder location.
|
29
|
+
# config.migrated_folder = Rails.root.join("custom", "migrated")
|
30
|
+
config.migrated_folder = Rails.root.join("tmp", "migrated")
|
27
31
|
end
|
@@ -55,11 +55,35 @@ namespace :actual_db_schema do # rubocop:disable Metrics/BlockLength
|
|
55
55
|
end
|
56
56
|
|
57
57
|
desc "Show the schema.rb diff annotated with the migrations that made the changes"
|
58
|
-
task :diff_schema_with_migrations, [
|
59
|
-
schema_path = args[:schema_path] || "db/schema.rb"
|
58
|
+
task :diff_schema_with_migrations, %i[schema_path migrations_path] => :environment do |_, args|
|
59
|
+
schema_path = args[:schema_path] || "./db/schema.rb"
|
60
60
|
migrations_path = args[:migrations_path] || "db/migrate"
|
61
61
|
|
62
62
|
schema_diff = ActualDbSchema::SchemaDiff.new(schema_path, migrations_path)
|
63
63
|
puts schema_diff.render
|
64
64
|
end
|
65
|
+
|
66
|
+
desc "Delete broken migration versions from the database"
|
67
|
+
task :delete_broken_versions, %i[versions database] => :environment do |_, args|
|
68
|
+
extend ActualDbSchema::OutputFormatter
|
69
|
+
|
70
|
+
if args[:versions]
|
71
|
+
versions = args[:versions].split(" ").map(&:strip)
|
72
|
+
versions.each do |version|
|
73
|
+
ActualDbSchema::Migration.instance.delete(version, args[:database])
|
74
|
+
puts colorize("[ActualDbSchema] Migration #{version} was successfully deleted.", :green)
|
75
|
+
rescue StandardError => e
|
76
|
+
puts colorize("[ActualDbSchema] Error deleting version #{version}: #{e.message}", :red)
|
77
|
+
end
|
78
|
+
elsif ActualDbSchema::Migration.instance.broken_versions.empty?
|
79
|
+
puts colorize("[ActualDbSchema] No broken versions found.", :gray)
|
80
|
+
else
|
81
|
+
begin
|
82
|
+
ActualDbSchema::Migration.instance.delete_all
|
83
|
+
puts colorize("[ActualDbSchema] All broken versions were successfully deleted.", :green)
|
84
|
+
rescue StandardError => e
|
85
|
+
puts colorize("[ActualDbSchema] Error deleting all broken versions: #{e.message}", :red)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
65
89
|
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.
|
4
|
+
version: 0.8.3
|
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-
|
11
|
+
date: 2025-03-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -155,12 +155,16 @@ files:
|
|
155
155
|
- README.md
|
156
156
|
- Rakefile
|
157
157
|
- actual_db_schema.gemspec
|
158
|
+
- app/controllers/actual_db_schema/broken_versions_controller.rb
|
158
159
|
- app/controllers/actual_db_schema/migrations_controller.rb
|
159
160
|
- app/controllers/actual_db_schema/phantom_migrations_controller.rb
|
161
|
+
- app/controllers/actual_db_schema/schema_controller.rb
|
162
|
+
- app/views/actual_db_schema/broken_versions/index.html.erb
|
160
163
|
- app/views/actual_db_schema/migrations/index.html.erb
|
161
164
|
- app/views/actual_db_schema/migrations/show.html.erb
|
162
165
|
- app/views/actual_db_schema/phantom_migrations/index.html.erb
|
163
166
|
- app/views/actual_db_schema/phantom_migrations/show.html.erb
|
167
|
+
- app/views/actual_db_schema/schema/index.html.erb
|
164
168
|
- app/views/actual_db_schema/shared/_js.html
|
165
169
|
- app/views/actual_db_schema/shared/_style.html
|
166
170
|
- config/routes.rb
|
@@ -192,6 +196,7 @@ files:
|
|
192
196
|
- lib/actual_db_schema/patches/migrator.rb
|
193
197
|
- lib/actual_db_schema/railtie.rb
|
194
198
|
- lib/actual_db_schema/schema_diff.rb
|
199
|
+
- lib/actual_db_schema/schema_diff_html.rb
|
195
200
|
- lib/actual_db_schema/schema_parser.rb
|
196
201
|
- lib/actual_db_schema/store.rb
|
197
202
|
- lib/actual_db_schema/version.rb
|