Showing preview only (352K chars total). Download the full file or copy to clipboard to get everything.
Repository: widefix/actual_db_schema
Branch: main
Commit: 0ea98c4afbf3
Files: 101
Total size: 324.0 KB
Directory structure:
gitextract_9537yhwc/
├── .github/
│ └── workflows/
│ └── main.yml
├── .gitignore
├── .rubocop.yml
├── Appraisals
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── actual_db_schema.gemspec
├── app/
│ ├── controllers/
│ │ └── actual_db_schema/
│ │ ├── broken_versions_controller.rb
│ │ ├── migrations_controller.rb
│ │ ├── phantom_migrations_controller.rb
│ │ └── schema_controller.rb
│ └── views/
│ └── actual_db_schema/
│ ├── broken_versions/
│ │ └── index.html.erb
│ ├── migrations/
│ │ ├── index.html.erb
│ │ └── show.html.erb
│ ├── phantom_migrations/
│ │ ├── index.html.erb
│ │ └── show.html.erb
│ ├── schema/
│ │ └── index.html.erb
│ └── shared/
│ ├── _js.html
│ └── _style.html
├── bin/
│ ├── console
│ └── setup
├── config/
│ └── routes.rb
├── docker/
│ ├── mysql-init/
│ │ └── create_secondary_db.sql
│ └── postgres-init/
│ └── create_secondary_db.sql
├── docker-compose.yml
├── gemfiles/
│ ├── rails.6.0.gemfile
│ ├── rails.6.1.gemfile
│ ├── rails.7.0.gemfile
│ ├── rails.7.1.gemfile
│ └── rails.edge.gemfile
├── lib/
│ ├── actual_db_schema/
│ │ ├── commands/
│ │ │ ├── base.rb
│ │ │ ├── list.rb
│ │ │ └── rollback.rb
│ │ ├── configuration.rb
│ │ ├── console_migrations.rb
│ │ ├── engine.rb
│ │ ├── failed_migration.rb
│ │ ├── git.rb
│ │ ├── git_hooks.rb
│ │ ├── instrumentation.rb
│ │ ├── migration.rb
│ │ ├── migration_context.rb
│ │ ├── migration_parser.rb
│ │ ├── multi_tenant.rb
│ │ ├── output_formatter.rb
│ │ ├── patches/
│ │ │ ├── migration_context.rb
│ │ │ ├── migration_proxy.rb
│ │ │ └── migrator.rb
│ │ ├── railtie.rb
│ │ ├── rollback_stats_repository.rb
│ │ ├── schema_diff.rb
│ │ ├── schema_diff_html.rb
│ │ ├── schema_parser.rb
│ │ ├── store.rb
│ │ ├── structure_sql_parser.rb
│ │ └── version.rb
│ ├── actual_db_schema.rb
│ ├── generators/
│ │ └── actual_db_schema/
│ │ └── templates/
│ │ └── actual_db_schema.rb
│ └── tasks/
│ ├── actual_db_schema.rake
│ ├── db.rake
│ └── test.rake
├── sig/
│ └── actual_db_schema.rbs
└── test/
├── controllers/
│ └── actual_db_schema/
│ ├── broken_versions_controller_db_storage_test.rb
│ ├── broken_versions_controller_test.rb
│ ├── migrations_controller_db_storage_test.rb
│ ├── migrations_controller_test.rb
│ ├── phantom_migrations_controller_db_storage_test.rb
│ ├── phantom_migrations_controller_test.rb
│ ├── schema_controller_db_storage_test.rb
│ └── schema_controller_test.rb
├── dummy_app/
│ ├── config/
│ │ └── .keep
│ ├── db/
│ │ ├── migrate/
│ │ │ └── .keep
│ │ └── migrate_secondary/
│ │ └── .keep
│ └── public/
│ └── 404.html
├── rake_task_console_migrations_db_storage_test.rb
├── rake_task_console_migrations_test.rb
├── rake_task_db_storage_full_test.rb
├── rake_task_db_storage_test.rb
├── rake_task_delete_broken_versions_db_storage_test.rb
├── rake_task_delete_broken_versions_test.rb
├── rake_task_git_hooks_install_db_storage_test.rb
├── rake_task_git_hooks_install_test.rb
├── rake_task_multi_tenant_db_storage_test.rb
├── rake_task_multi_tenant_test.rb
├── rake_task_schema_diff_db_storage_test.rb
├── rake_task_schema_diff_test.rb
├── rake_task_secondary_db_storage_test.rb
├── rake_task_secondary_test.rb
├── rake_task_test.rb
├── rake_tasks_all_databases_db_storage_test.rb
├── rake_tasks_all_databases_test.rb
├── support/
│ └── test_utils.rb
├── test_actual_db_schema.rb
├── test_actual_db_schema_db_storage_test.rb
├── test_database_filtering.rb
├── test_helper.rb
└── test_migration_context.rb
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/main.yml
================================================
name: Ruby
on:
push:
branches:
- main
pull_request:
jobs:
build:
runs-on: ubuntu-latest
name: Ruby ${{ matrix.ruby }}, Rails ${{ matrix.rails }}
strategy:
matrix:
include:
- { ruby: '2.7', rails: '6.0' }
- { ruby: '2.7', rails: '6.1' }
- { ruby: '3.0', rails: '6.1' }
- { ruby: '3.1', rails: '7.0' }
- { ruby: '3.2', rails: '7.1' }
- { ruby: '3.3', rails: '7.1' }
- { ruby: '3.3', rails: 'edge' }
env:
BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/rails.${{ matrix.rails }}.gemfile
steps:
- uses: actions/checkout@v2
- name: Install SQLite3 Development Libraries
run: sudo apt-get update && sudo apt-get install -y libsqlite3-dev docker-compose
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- name: Run Tests with All Adapters
run: bundle exec rake test:all
rubocop:
runs-on: ubuntu-latest
name: Rubocop
steps:
- uses: actions/checkout@v2
- uses: ruby/setup-ruby@v1
with:
ruby-version: 2.7
bundler-cache: true
- run: bundle exec rubocop
================================================
FILE: .gitignore
================================================
/.bundle/
/.yardoc
/_yardoc/
/coverage/
/doc/
/pkg/
/spec/reports/
/tmp/
/test/dummy_app/tmp/
/test/dummy_app/custom/
/test/dummy_app/db/**/*.rb
/test/dummy_app/db/structure.sql
/test/dummy_app/config/database.yml
.ruby-version
.ruby-gemset
/gemfiles/*.gemfile.lock
================================================
FILE: .rubocop.yml
================================================
AllCops:
TargetRubyVersion: 2.7
Exclude:
- gemfiles/*
- test/dummy_app/**/*
- vendor/**/*
Style/StringLiterals:
Enabled: true
EnforcedStyle: double_quotes
Style/StringLiteralsInInterpolation:
Enabled: true
EnforcedStyle: double_quotes
Layout/LineLength:
Max: 120
Metrics/BlockLength:
Exclude:
- test/**/*
- actual_db_schema.gemspec
Metrics/MethodLength:
Max: 15
Exclude:
- test/**/*
Metrics/ClassLength:
Enabled: false
Metrics/ModuleLength:
Enabled: false
Metrics/AbcSize:
Max: 25
Metrics/CyclomaticComplexity:
Max: 10
Metrics/PerceivedComplexity:
Max: 10
Metrics/AbcSize:
Enabled: false
================================================
FILE: Appraisals
================================================
# frozen_string_literal: true
%w[6.0 6.1 7.0 7.1].each do |version|
appraise "rails.#{version}" do
gem "activerecord", "~> #{version}.0"
gem "activesupport", "~> #{version}.0"
end
end
appraise "rails.edge" do
gem "rails", ">= 7.2.0.beta"
gem "activerecord", ">= 7.2.0.beta"
gem "activesupport", ">= 7.2.0.beta"
end
================================================
FILE: CHANGELOG.md
================================================
## [0.9.1] - 2026-02-25
- Support schema diffs for `structure.sql`
- Add an option to exclude specific databases from the gem's visibility scope
- Fix a crash when the database is not available at application startup
- Add instrumentation tooling to track stats about rolled-back phantom migrations
## [0.9.0] - 2026-01-27
- Store migration files in the DB to avoid reliance on the filesystem, enabling CI/CD usage on platforms with ephemeral storage (e.g., Heroku, Docker).
## [0.8.6] - 2025-05-21
- Fix gem installtion with git hooks
- Update README
## [0.8.5] - 2025-04-10
- Fix the gem working on projects without git
## [0.8.4] - 2025-03-20
- Fix initializer file that can break other bundle groups that development
- Use prism gem instead of parser for Ruby 3.4 compatibility
## [0.8.3] - 2025-03-03
- View Schema with Migration Annotations in the UI
- Clean Up Broken Migrations
- Filter Migrations in the UI
- Customize Your Migrated Folder Location
## [0.8.2] - 2025-02-06
- Show migration name in the schema.rb diff that caused the change
- Easy way to run DDL migration methods in Rails console
## [0.8.1] - 2025-01-15
- Support for multiple database schemas, ensuring compatibility with multi-tenant applications using the apartment gem or similar solutions
- DSL for configuring the gem, simplifying setup and customization
- Rake task added to initialize the gem
- Improved the post-checkout git hook to run only when switching branches, reducing unnecessary executions during file checkouts
- Fixed the changelog link in the gemspec, ensuring Rubygems points to the correct file and the link works
## [0.8.0] - 2024-12-30
- Enhanced Console Visibility: Automatically rolled-back phantom migrations now provide clearer and more visible logs in the console
- Git Hooks for Branch Management: Introduced hooks that automatically rollback phantom migrations after checking out a branch. Additionally, the schema migration rake task can now be executed automatically upon branch checkout
- Temporary Folder Cleanup: Rolled-back phantom migrations are now automatically deleted from the temporary folder after rollback
- Acronym Support in Phantom Migration Names: Resolved an issue where phantom migrations with acronyms in their names, defined in other branches, couldn't be rolled back automatically. These are now handled seamlessly
## [0.7.9] - 2024-09-07
- Don't stop if a phantom migration rollback fails
- Improve failed rollback of phantom migrations report
## [0.7.8] - 2024-08-07
- Make UI working without assets pipeline
## [0.7.7] - 2024-07-22
- Unlock compatibility with Rails versions earlier than 6.0
## [0.7.6] - 2024-07-22
- Added UI
- Added environment variable `ACTUAL_DB_SCHEMA_UI_ENABLED` to enable/disable the UI in specific environments
- Added configuration option `ActualDbSchema.config[:ui_enabled]` to enable/disable the UI in specific environments
## [0.7.5] - 2024-06-20
- Added db:rollback_migrations:manual task to manually rolls back phantom migrations one by one
## [0.7.4] - 2024-06-06
- Rails 7.2 support added
- Rails 6.0 support added
## [0.7.3] - 2024-04-06
- add multipe databases support
## [0.7.2] - 2024-03-30
- update title and description in Rubygems
## [0.7.1] - 2024-03-19
- add csv as a dependency since Ruby 3.3 has removed it from the standard library
## [0.7.0] - 2024-01-18
- db:phantom_migrations displays the branch in which the phantion migration was run
## [0.6.0] - 2024-01-03
- Added db:phantom_migrations task to display phantom migrations
- Updated README
## [0.5.0] - 2023-11-06
- Rails 7.1 support added
## [0.4.0] - 2023-07-05
- rollback migrations in the reversed order
## [0.3.0] - 2023-01-23
- add Rails 6 and older support
## [0.2.0] - 2022-10-19
- Catch exceptions about irreversible migrations and show a warning
- Namespace all patches into gem module
- Fix typo in a module name with a patch
- Use guard clause
## [0.1.0] - 2022-10-16
- Initial release
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at ka8725@gmail.com. All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of actions.
**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
================================================
FILE: Gemfile
================================================
# frozen_string_literal: true
source "https://rubygems.org"
# Specify your gem's dependencies in actual_db_schema.gemspec
gemspec
gem "activerecord", "~> 7.1.0"
gem "activesupport", "~> 7.1.0"
gem "minitest", "~> 5.0"
gem "rake"
gem "rubocop", "~> 1.21"
================================================
FILE: LICENSE.txt
================================================
The MIT License (MIT)
Copyright (c) 2022 Andrei Kaleshka
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: README.md
================================================
[](https://badge.fury.io/rb/actual_db_schema)
# ActualDbSchema
**Stop database headaches when switching Git branches in Rails**
Keep your database schema perfectly synchronized across Git branches, eliminate broken tests and schema conflicts, and save wasted hours on phantom migrations.
## 🚀 What You Get
- **Zero Manual Work**: Switch branches freely - phantom migrations roll back automatically
- **No More Schema Conflicts**: Clean `schema.rb`/`structure.sql` diffs every time, no irrelevant changes
- **Error Prevention**: Eliminates `ActiveRecord::NotNullViolation` and similar errors when switching branches
- **Time Savings**: Stop hunting down which branch has the problematic migration
- **Team Productivity**: Everyone stays focused on coding, not database maintenance
- **Staging/Sandbox Sync**: Keep staging and sandbox databases aligned with your current branch code
- **Visual Management**: Web UI to view and manage migrations across all databases
<img width="3024" height="1886" alt="Visual management of Rails DB migrations with ActualDbSchema" src="https://github.com/user-attachments/assets/87cfb7b4-6380-4dad-ab18-6a0633f561b5" />
And you get all of that with **zero** changes to your workflow!
## 🎯 The Problem This Solves
**Before ActualDbSchema:**
1. Work on Branch A → Add migration → Run migration
2. Switch to Branch B → Code breaks with database errors
3. Manually find and rollback the "phantom" migration
4. Deal with irrelevant `schema.rb` diffs
5. Repeat this tedious process constantly
**After ActualDbSchema:**
1. Work on any branch → Add migrations as usual
2. Switch branches freely → Everything just works
3. Focus on building features, not fixing database issues
## 🌟 Complete Feature Set
### Core Migration Management
- **Phantom Migration Detection**: Automatically identifies migrations from other branches
- **Smart Rollback**: Rolls back phantom migrations in correct dependency order
- **Irreversible Migration Handling**: Safely handles and reports irreversible migrations
- **Multi-Database Support**: Works seamlessly with multiple database configurations
- **Schema Format Agnostic**: Supports both `schema.rb` and `structure.sql`
### Automation & Git Integration
- **Automatic Rollback on Migration**: Phantom migrations roll back when running `db:migrate`
- **Git Hook Integration**: Optional automatic rollback when switching branches
- **Zero Configuration**: Works out of the box with sensible defaults
- **Custom Migration Storage**: Configurable location for storing executed migrations
### Web Interface & Management
- **Migration Dashboard**: Visual overview of all migrations across databases
- **Phantom Migration Browser**: Easy-to-use interface for viewing phantom migrations
- **One-Click Rollback**: Rollback individual or all phantom migrations via web UI
- **Broken Version Cleanup**: Identify and remove orphaned migration records
- **Schema Diff Viewer**: Visual diff of schema changes with migration annotations
### Developer Tools
- **Console Migrations**: Run migration commands directly in Rails console
- **Schema Diff Analysis**: Annotated diffs showing which migrations caused changes
- **Migration Search & Filter**: Find specific migrations across all databases
- **Detailed Migration Info**: View migration status, branch, and database information
### Team & Environment Support
- **Multi-Tenant Compatible**: Works with apartment gem and similar multi-tenant setups
- **Environment Flexibility**: Enable/disable features per environment
- **Team Synchronization**: Keeps all team members' databases in sync
- **CI/CD Friendly**: No interference with deployment pipelines
### Manual Control Options
- **Manual Rollback Mode**: Disable automatic rollback for full manual control
- **Selective Rollback**: Choose which phantom migrations to rollback
- **Interactive Mode**: Step-by-step confirmation for each rollback operation
- **Rake Task Integration**: Full set of rake tasks for command-line management
## ⚡ Quick Start
Add to your Gemfile:
```ruby
group :development do
gem "actual_db_schema"
end
```
Install and configure:
```sh
bundle install
rails actual_db_schema:install
```
That's it! Now just run `rails db:migrate` as usual - phantom migrations roll back automatically.
## 🔧 How It Works
This gem stores all run migrations with their code in the `tmp/migrated` folder. Whenever you perform a schema dump, it rolls back the *phantom migrations*.
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).
Therefore, all you do is run rails `db:migrate` in your current branch. `actual_db_schema` will ensure the DB schema is up-to-date. You'll never have an inaccurate `schema.rb` file again.
## Installation
Add this line to your application's Gemfile:
```ruby
group :development do
gem "actual_db_schema"
end
```
And then execute:
$ bundle install
If you cannot commit changes to the repo or Gemfile, consider the local Gemfile installation described in [this post](https://blog.widefix.com/personal-gemfile-for-development/).
Next, generate your ActualDbSchema initializer file by running:
```sh
rails actual_db_schema:install
```
This will create a `config/initializers/actual_db_schema.rb` file that lists all available configuration options, allowing you to customize them as needed. The installation process will also prompt you to install the post-checkout Git hook, which automatically rolls back phantom migrations when switching branches. If enabled, this hook will run the schema actualization rake task every time you switch branches, which can slow down branch changes. Therefore, you might not always want this automatic actualization on every switch; in that case, running `rails db:migrate` manually provides a faster, more controlled alternative.
For more details on the available configuration options, see the sections below.
## Usage
Just run `rails db:migrate` inside the current branch. It will roll back all phantom migrations for all configured databases in your `database.yml.`
> [!WARNING]
> This solution implies that all migrations are reversible. The irreversible migrations should be solved manually. At the moment, the gem ignores them. You will see warnings in the terminal for each irreversible migrations.
The gem offers the following rake tasks that can be manually run according to your preferences:
- `rails db:rollback_branches` - run it to manually rolls back phantom migrations.
- `rails db:rollback_branches:manual` - run it to manually rolls back phantom migrations one by one.
- `rails db:phantom_migrations` - displays a list of phantom migrations.
## 🎛️ Configuration Options
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:
### 1. Using Environment Variable
Set the environment variable `ACTUAL_DB_SCHEMA_MIGRATED_FOLDER` to your desired folder path:
```sh
export ACTUAL_DB_SCHEMA_MIGRATED_FOLDER="custom/migrated"
```
### 2. Using Initializer
Add the following line to your initializer file (`config/initializers/actual_db_schema.rb`):
```ruby
config.migrated_folder = Rails.root.join("custom", "migrated")
```
### 3. Store migrations in the database
If you want to share executed migrations across environments (e.g., staging or sandboxes),
store them in the main database instead of the local filesystem:
```ruby
config.migrations_storage = :db
```
Or via environment variable:
```sh
export ACTUAL_DB_SCHEMA_MIGRATIONS_STORAGE="db"
```
If both are set, the initializer setting (`config.migrations_storage`) takes precedence.
## 🌐 Web Interface
Access the migration management UI at:
```
http://localhost:3000/rails/phantom_migrations
```
View and manage:
- **Migration Overview**: See all executed migrations with their status, branch, and database
- **Phantom Migrations**: Identify migrations from other branches that need rollback
- **Migration Source Code**: Browse the source code of every migration ever run (including the phantom ones)
- **One-Click Actions**: Rollback or migrate individual migrations directly from the UI
- **Broken Versions**: Detect and clean up orphaned migration records safely
- **Schema Diffs**: Visual diff of schema changes annotated with their source migrations
## UI options
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:
### 1. Using Environment Variable
Set the environment variable `ACTUAL_DB_SCHEMA_UI_ENABLED` to `true`:
```sh
export ACTUAL_DB_SCHEMA_UI_ENABLED=true
```
### 2. Using Initializer
Add the following line to your initializer file (`config/initializers/actual_db_schema.rb`):
```ruby
config.ui_enabled = true
```
> With this option, the UI can be disabled for all environments or be enabled in specific ones.
## Disabling Automatic Rollback
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:
### 1. Using Environment Variable
Set the environment variable `ACTUAL_DB_SCHEMA_AUTO_ROLLBACK_DISABLED` to `true`:
```sh
export ACTUAL_DB_SCHEMA_AUTO_ROLLBACK_DISABLED=true
```
### 2. Using Initializer
Add the following line to your initializer file (`config/initializers/actual_db_schema.rb`):
```ruby
config.auto_rollback_disabled = true
```
## Rollback Instrumentation
ActualDbSchema emits an `ActiveSupport::Notifications` event for each successful phantom rollback:
- Event name: `rollback_migration.actual_db_schema`
- Event is always emitted when a phantom rollback succeeds
### Event payload
| Field | Description |
|-------|-------------|
| `version` | Migration version that was rolled back |
| `name` | Migration class name |
| `database` | Current database name from Active Record config |
| `schema` | Tenant schema name (or `nil` for default schema) |
| `branch` | Branch associated with the migration metadata |
| `manual_mode` | Whether rollback was run in manual mode |
### Subscribing to rollback events
You can subscribe to rollback events in your initializer to track statistics or perform custom actions:
```ruby
# config/initializers/actual_db_schema.rb
ActiveSupport::Notifications.subscribe(ActualDbSchema::Instrumentation::ROLLBACK_EVENT) do |_name, _start, _finish, _id, payload|
ActualDbSchema::RollbackStatsRepository.record(payload)
end
```
The `RollbackStatsRepository` persists rollback events to a database table (`actual_db_schema_rollback_events`) that is automatically excluded from schema dumps.
Read aggregated stats at runtime:
```ruby
ActualDbSchema::RollbackStatsRepository.stats
# => { total: 3, by_database: { "primary" => 3 }, by_schema: { "default" => 3 }, by_branch: { "main" => 3 } }
ActualDbSchema::RollbackStatsRepository.total_rollbacks
# => 3
ActualDbSchema::RollbackStatsRepository.reset!
# Clears all recorded stats
```
## Automatic Phantom Migration Rollback On Branch Switch
By default, the automatic rollback of migrations on branch switch is disabled. If you prefer to automatically rollback phantom migrations whenever you switch branches with `git checkout`, you can enable it in two ways:
### 1. Using Environment Variable
Set the environment variable `ACTUAL_DB_SCHEMA_GIT_HOOKS_ENABLED` to `true`:
```sh
export ACTUAL_DB_SCHEMA_GIT_HOOKS_ENABLED=true
```
### 2. Using Initializer
Add the following line to your initializer file (`config/initializers/actual_db_schema.rb`):
```ruby
config.git_hooks_enabled = true
```
### Installing the Post-Checkout Hook
After enabling Git hooks in your configuration, run the rake task to install the post-checkout hook:
```sh
rake actual_db_schema:install_git_hooks
```
This task will prompt you to choose one of the three options:
1. Rollback phantom migrations with `db:rollback_branches`
2. Migrate up to the latest schema with `db:migrate`
3. Skip installing git hook
Based on your selection, a post-checkout hook will be installed or updated in your `.git/hooks` folder.
## Excluding Databases from Processing
**For Rails 6.1+ applications using multiple databases** (especially with infrastructure databases like Solid Queue, Solid Cable, or Solid Cache), you can exclude specific databases from ActualDbSchema's processing to prevent connection conflicts.
### Why You Might Need This
Modern Rails applications often use the `connects_to` pattern for infrastructure databases. These databases maintain their own isolated connection pools, and ActualDbSchema's global connection switching can interfere with active queries. This is particularly common with:
- **Solid Queue** (Rails 8 default job backend)
- **Solid Cable** (WebSocket connections)
- **Solid Cache** (caching infrastructure)
### Method 1: Using `excluded_databases` Configuration
Explicitly exclude databases by name in your initializer:
```ruby
# config/initializers/actual_db_schema.rb
ActualDbSchema.configure do |config|
config.excluded_databases = [:queue, :cable, :cache]
end
```
### Method 2: Using Environment Variable
Set the environment variable `ACTUAL_DB_SCHEMA_EXCLUDED_DATABASES` with a comma-separated list:
```sh
export ACTUAL_DB_SCHEMA_EXCLUDED_DATABASES="queue,cable,cache"
```
**Note:** If both the environment variable and the configuration setting in the initializer are provided, the configuration setting takes precedence as it's applied after the default settings are loaded.
## Multi-Tenancy Support
If your application leverages multiple schemas for multi-tenancy — such as those implemented by the [apartment](https://github.com/influitive/apartment) gem or similar solutions — you can configure ActualDbSchema to handle migrations across all schemas. To do so, add the following configuration to your initializer file (`config/initializers/actual_db_schema.rb`):
```ruby
config.multi_tenant_schemas = -> { # list of all active schemas }
```
### Example:
```ruby
config.multi_tenant_schemas = -> { ["public", "tenant1", "tenant2"] }
```
## Schema Diff with Migration Annotations
If `schema.rb` generates a diff, it can be helpful to find out which migrations caused the changes. This helps you decide whether to resolve the diff on your own or discuss it with your teammates to determine the next steps. The `diff_schema_with_migrations` Rake task generates a diff of the `schema.rb` file, annotated with the migrations responsible for each change. This makes it easier to trace which migration introduced a specific schema modification, enabling faster and more informed decision-making regarding how to handle the diff.
By default, the task uses `db/schema.rb` and `db/migrate` as the schema and migrations paths. You can also provide custom paths as arguments.
Alternatively, if you use Web UI, you can see this diff at `http://localhost:3000/rails/schema`. This way is often more convenient than running the Rake task manually.
### Usage
Run the task with default paths:
```sh
rake actual_db_schema:diff_schema_with_migrations
```
Run the task with custom paths:
```sh
rake actual_db_schema:diff_schema_with_migrations[path/to/custom_schema.rb, path/to/custom_migrations]
```
## Console Migrations
Sometimes, it's necessary to modify the database without creating migration files. This can be useful for fixing a corrupted schema, conducting experiments (such as adding and removing indexes), or quickly adjusting the schema in development. This gem allows you to run the same commands used in migrations directly in the Rails console.
By default, Console Migrations is disabled. You can enable it in two ways:
### 1. Using Environment Variable
Set the environment variable `ACTUAL_DB_SCHEMA_CONSOLE_MIGRATIONS_ENABLED` to `true`:
```sh
export ACTUAL_DB_SCHEMA_CONSOLE_MIGRATIONS_ENABLED=true
```
### 2. Using Initializer
Add the following line to your initializer file (`config/initializers/actual_db_schema.rb`):
```ruby
config.console_migrations_enabled = true
```
### Usage
Once enabled, you can run migration commands directly in the Rails console:
```ruby
# Create a new table
create_table :posts do |t|
t.string :title
end
# Add a column
add_column :users, :age, :integer
# Remove an index
remove_index :users, :email
# Rename a column
rename_column :users, :username, :handle
```
## Delete Broken Migrations
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.
You can delete broken migrations using either of the following methods:
### 1. Using the UI
Navigate to the following URL in your web browser:
```
http://localhost:3000/rails/broken_versions
```
This page lists all broken versions and provides an option to delete them.
### 2. Using a Rake Task
To delete all broken migrations, run:
```sh
rake actual_db_schema:delete_broken_versions
```
To delete specific migrations, pass the migration version(s) and optionally a database:
```sh
rake actual_db_schema:delete_broken_versions[<version>, <version>]
```
- `<version>` – The migration version(s) to delete (space-separated if multiple).
- `<database>` (optional) – Specify a database if using multiple databases.
#### Examples:
```sh
# Delete all broken migrations
rake actual_db_schema:delete_broken_versions
# Delete specific migrations
rake actual_db_schema:delete_broken_versions["20250224103352 20250224103358"]
# Delete specific migrations from a specific database
rake actual_db_schema:delete_broken_versions["20250224103352 20250224103358", "primary"]
```
## 🏗️ Development
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.
To install this gem onto your local machine, run `bundle exec rake install`.
To release a new version do the following in the order:
- update the version number in `version.rb`;
- update the CHANGELOG;
- `bundle install` to update `Gemfile.lock`;
- make the commit and push;
- 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);
- [announce the new release on GitHub](https://github.com/widefix/actual_db_schema/releases);
- close the milestone on GitHub.
### Running Tests with Specific Rails Versions
The following versions can be specifically tested using Appraisal
- 6.0
- 6.1
- 7.0
- 7.1
- edge
To run tests with a specific version of Rails using Appraisal:
- Run all tests with Rails 6.0:
```sh
bundle exec appraisal rails.6.0 rake test
```
- Run tests for a specific file:
```sh
bundle exec appraisal rails.6.0 rake test TEST=test/rake_task_test.rb
```
- Run a specific test:
```sh
bundle exec appraisal rails.6.0 rake test TEST=test/rake_task_test.rb TESTOPTS="--name=/db::db:rollback_branches#test_0003_keeps/"
```
By default, `rake test` runs tests using `SQLite3`. To explicitly run tests with `SQLite3`, `PostgreSQL`, or `MySQL`, you can use the following tasks:
- Run tests with `SQLite3`:
```sh
bundle exec rake test:sqlite3
```
- Run tests with `PostgreSQL` (requires Docker):
```sh
bundle exec rake test:postgresql
```
- Run tests with `MySQL` (requires Docker):
```sh
bundle exec rake test:mysql2
```
- Run tests for all supported adapters:
```sh
bundle exec rake test:all
```
## Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/widefix/actual_db_schema. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/widefix/actual_db_schema/blob/master/CODE_OF_CONDUCT.md).
## License
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
## Code of Conduct
Everyone interacting in the ActualDbSchema project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/widefix/actual_db_schema/blob/master/CODE_OF_CONDUCT.md).
================================================
FILE: Rakefile
================================================
# frozen_string_literal: true
require "bundler/gem_tasks"
require "rake/testtask"
load "lib/tasks/test.rake"
Rake::TestTask.new(:test) do |t|
t.libs << "test"
t.libs << "lib"
t.test_files = FileList["test/**/test_*.rb", "test/**/*_test.rb"]
end
require "rubocop/rake_task"
RuboCop::RakeTask.new
task default: %i[test]
================================================
FILE: actual_db_schema.gemspec
================================================
# frozen_string_literal: true
require_relative "lib/actual_db_schema/version"
Gem::Specification.new do |spec|
spec.name = "actual_db_schema"
spec.version = ActualDbSchema::VERSION
spec.authors = ["Andrei Kaleshka"]
spec.email = ["ka8725@gmail.com"]
spec.summary = "Keep DB schema in sync across branches effortlessly."
spec.description = <<~DESC
Keep your DB schema in sync across all branches effortlessly.
Install once, then use `rails db:migrate` normally — the gem handles phantom migration rollback automatically, eliminating schema conflicts and inconsistent database states.
Stop wasting hours on DB maintenance locally, CI, staging/sandbox, or even production.
DESC
spec.homepage = "https://blog.widefix.com/actual-db-schema/"
spec.license = "MIT"
spec.required_ruby_version = ">= 2.7.0"
# spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
spec.metadata["homepage_uri"] = spec.homepage
spec.metadata["source_code_uri"] = "https://github.com/widefix/actual_db_schema"
spec.metadata["changelog_uri"] = "https://github.com/widefix/actual_db_schema/blob/main/CHANGELOG.md"
# Specify which files should be added to the gem when it is released.
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
spec.files = Dir.chdir(File.expand_path(__dir__)) do
`git ls-files -z`.split("\x0").reject do |f|
(f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
end
end
spec.bindir = "exe"
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
# Uncomment to register a new dependency of your gem
spec.add_runtime_dependency "activerecord"
spec.add_runtime_dependency "activesupport"
spec.add_runtime_dependency "ast"
spec.add_runtime_dependency "csv"
spec.add_runtime_dependency "parser"
spec.add_runtime_dependency "prism"
spec.add_development_dependency "appraisal"
spec.add_development_dependency "debug"
spec.add_development_dependency "rails"
spec.add_development_dependency "sqlite3"
spec.post_install_message = <<~MSG
Thank you for installing ActualDbSchema!
Next steps:
1. Run `rake actual_db_schema:install` to generate the initializer file and install
the post-checkout Git hook for automatic phantom migration rollback when switching branches.
2. Or, if you prefer environment variables, skip this step.
For more information, see the README.
MSG
# For more information and examples about making a new gem, check out our
# guide at: https://bundler.io/guides/creating_gem.html
end
================================================
FILE: app/controllers/actual_db_schema/broken_versions_controller.rb
================================================
# frozen_string_literal: true
module ActualDbSchema
# Controller for managing broken migration versions.
class BrokenVersionsController < ActionController::Base
protect_from_forgery with: :exception
skip_before_action :verify_authenticity_token
def index; end
def delete
handle_delete(params[:id], params[:database])
redirect_to broken_versions_path
end
def delete_all
handle_delete_all
redirect_to broken_versions_path
end
private
def handle_delete(id, database)
ActualDbSchema::Migration.instance.delete(id, database)
flash[:notice] = "Migration #{id} was successfully deleted."
rescue StandardError => e
flash[:alert] = e.message
end
def handle_delete_all
ActualDbSchema::Migration.instance.delete_all
flash[:notice] = "All broken versions were successfully deleted."
rescue StandardError => e
flash[:alert] = e.message
end
helper_method def broken_versions
@broken_versions ||= ActualDbSchema::Migration.instance.broken_versions
end
end
end
================================================
FILE: app/controllers/actual_db_schema/migrations_controller.rb
================================================
# frozen_string_literal: true
module ActualDbSchema
# Controller to display the list of migrations for each database connection.
class MigrationsController < ActionController::Base
protect_from_forgery with: :exception
skip_before_action :verify_authenticity_token
def index; end
def show
render file: "#{Rails.root}/public/404.html", layout: false, status: :not_found unless migration
end
def rollback
handle_rollback(params[:id], params[:database])
redirect_to migrations_path
end
def migrate
handle_migrate(params[:id], params[:database])
redirect_to migrations_path
end
private
def handle_rollback(id, database)
ActualDbSchema::Migration.instance.rollback(id, database)
flash[:notice] = "Migration #{id} was successfully rolled back."
rescue StandardError => e
flash[:alert] = e.message
end
def handle_migrate(id, database)
ActualDbSchema::Migration.instance.migrate(id, database)
flash[:notice] = "Migration #{id} was successfully migrated."
rescue StandardError => e
flash[:alert] = e.message
end
helper_method def migrations
@migrations ||= ActualDbSchema::Migration.instance.all
query = params[:query].to_s.strip.downcase
return @migrations if query.blank?
@migrations.select do |migration|
file_name_matches = migration[:filename].include?(query)
content_matches = begin
File.read(migration[:filename]).downcase.include?(query)
rescue StandardError
false
end
file_name_matches || content_matches
end
end
helper_method def migration
@migration ||= ActualDbSchema::Migration.instance.find(params[:id], params[:database])
end
end
end
================================================
FILE: app/controllers/actual_db_schema/phantom_migrations_controller.rb
================================================
# frozen_string_literal: true
module ActualDbSchema
# Controller to display the list of phantom migrations for each database connection.
class PhantomMigrationsController < ActionController::Base
protect_from_forgery with: :exception
skip_before_action :verify_authenticity_token
def index; end
def show
render file: "#{Rails.root}/public/404.html", layout: false, status: :not_found unless phantom_migration
end
def rollback
handle_rollback(params[:id], params[:database])
redirect_to phantom_migrations_path
end
def rollback_all
handle_rollback_all
redirect_to phantom_migrations_path
end
private
def handle_rollback(id, database)
ActualDbSchema::Migration.instance.rollback(id, database)
flash[:notice] = "Migration #{id} was successfully rolled back."
rescue StandardError => e
flash[:alert] = e.message
end
def handle_rollback_all
ActualDbSchema::Migration.instance.rollback_all
flash[:notice] = "Migrations was successfully rolled back."
rescue StandardError => e
flash[:alert] = e.message
end
helper_method def phantom_migrations
@phantom_migrations ||= ActualDbSchema::Migration.instance.all_phantom
end
helper_method def phantom_migration
@phantom_migration ||= ActualDbSchema::Migration.instance.find(params[:id], params[:database])
end
end
end
================================================
FILE: app/controllers/actual_db_schema/schema_controller.rb
================================================
# frozen_string_literal: true
module ActualDbSchema
# Controller to display the database schema diff.
class SchemaController < ActionController::Base
protect_from_forgery with: :exception
skip_before_action :verify_authenticity_token
def index; end
private
helper_method def schema_diff_html
schema_path = Rails.configuration.active_record.schema_format == :sql ? "./db/structure.sql" : "./db/schema.rb"
schema_diff = ActualDbSchema::SchemaDiffHtml.new(schema_path, "db/migrate")
schema_diff.render_html(params[:table])
end
end
end
================================================
FILE: app/views/actual_db_schema/broken_versions/index.html.erb
================================================
<!DOCTYPE html>
<html>
<head>
<title>Broken Versions</title>
<%= render partial: 'actual_db_schema/shared/js' %>
<%= render partial: 'actual_db_schema/shared/style' %>
</head>
<body>
<div>
<% flash.each do |key, message| %>
<div class="flash <%= key %>"><%= message %></div>
<% end %>
<h2>Broken Versions</h2>
<p>
These are versions that were migrated in the database, but the corresponding migration file is missing.
You can safely delete them from the database to clean up.
</p>
<div class="top-buttons">
<%= link_to 'All Migrations', migrations_path, class: "top-button" %>
<% if broken_versions.present? %>
<%= button_to '✖ Delete all',
delete_all_broken_versions_path,
method: :post,
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?' },
class: 'button migration-action' %>
<% end %>
</div>
<% if broken_versions.present? %>
<table>
<thead>
<tr>
<th>Status</th>
<th>Migration ID</th>
<th>Branch</th>
<th>Database</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<% broken_versions.each do |version| %>
<tr class="migration-row phantom">
<td><%= version[:status] %></td>
<td><%= version[:version] %></td>
<td><%= version[:branch] %></td>
<td><%= version[:database] %></td>
<td>
<div class='button-container'>
<%= button_to '✖ Delete',
delete_broken_version_path(id: version[:version], database: version[:database]),
method: :post,
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?' },
class: 'button migration-action' %>
</div>
</td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<p>No broken versions found.</p>
<% end %>
</div>
</body>
</html>
================================================
FILE: app/views/actual_db_schema/migrations/index.html.erb
================================================
<!DOCTYPE html>
<html>
<head>
<title>Migrations</title>
<%= render partial: 'actual_db_schema/shared/js' %>
<%= render partial: 'actual_db_schema/shared/style' %>
</head>
<body>
<div>
<% flash.each do |key, message| %>
<div class="flash <%= key %>"><%= message %></div>
<% end %>
<h2>Migrations</h2>
<p>
<span style="background-color: #ffe6e6; padding: 0 5px;">Red rows</span> represent phantom migrations.
</p>
<div class="container">
<div class="top-controls">
<div class="top-buttons">
<%= link_to 'Phantom Migrations', phantom_migrations_path, class: "top-button" %>
<%= link_to 'Broken Versions', broken_versions_path, class: "top-button" %>
<%= link_to 'View Schema', schema_path, class: "top-button" %>
</div>
<div class="top-search">
<%= form_tag migrations_path, method: :get, class: "search-form" do %>
<span class="search-icon">🔍</span>
<%= text_field_tag :query, params[:query], placeholder: "Search migrations by name or content", class: "search-input" %>
<% end %>
</div>
</div>
<% if migrations.present? %>
<table>
<thead>
<tr>
<th>Status</th>
<th>Migration ID</th>
<th>Name</th>
<th>Branch</th>
<th>Database</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<% migrations.each do |migration| %>
<tr class="migration-row <%= migration[:phantom] ? 'phantom' : 'normal' %>">
<td><%= migration[:status] %></td>
<td><%= migration[:version] %></td>
<td>
<div class="truncate-text" title="<%= migration[:name] %>">
<%= migration[:name] %>
</div>
</td>
<td><%= migration[:branch] %></td>
<td><%= migration[:database] %></td>
<td>
<div class='button-container'>
<%= link_to '👁 Show',
migration_path(id: migration[:version], database: migration[:database]),
class: 'button' %>
<%= button_to '⎌ Rollback',
rollback_migration_path(id: migration[:version], database: migration[:database]),
method: :post,
class: 'button migration-action',
style: ('display: none;' if migration[:status] == "down") %>
<%= button_to '⬆ Migrate',
migrate_migration_path(id: migration[:version], database: migration[:database]),
method: :post,
class: 'button migration-action',
style: ('display: none;' if migration[:status] == "up" || migration[:phantom]) %>
</div>
</td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<p>No migrations found.</p>
<% end %>
</div>
</div>
</body>
</html>
================================================
FILE: app/views/actual_db_schema/migrations/show.html.erb
================================================
<!DOCTYPE html>
<html>
<head>
<title>Migration Details</title>
<%= render partial: 'actual_db_schema/shared/js' %>
<%= render partial: 'actual_db_schema/shared/style' %>
</head>
<body>
<div>
<% flash.each do |key, message| %>
<div class="flash <%= key %>"><%= message %></div>
<% end %>
<h2>Migration <%= migration[:name] %> Details</h2>
<table>
<tbody>
<tr>
<th>Status</th>
<td><%= migration[:status] %></td>
</tr>
<tr>
<th>Migration ID</th>
<td><%= migration[:version] %></td>
</tr>
<tr>
<th>Branch</th>
<td><%= migration[:branch] %></td>
</tr>
<tr>
<th>Database</th>
<td><%= migration[:database] %></td>
</tr>
<tr>
<th>Path</th>
<td>
<%= migration[:filename] %>
<% source = migration[:source].to_s %>
<% if source.present? %>
<span class="source-badge"><%= source.upcase %></span>
<% end %>
</td>
</tr>
</tbody>
</table>
<h3>Migration Code</h3>
<div>
<pre><%= File.read(migration[:filename]) %></pre>
</div>
<div class='button-container'>
<%= link_to '← Back', migrations_path, class: 'button' %>
<%= button_to '⎌ Rollback',
rollback_migration_path(id: migration[:version], database: migration[:database]),
method: :post,
class: 'button migration-action',
style: ('display: none;' if migration[:status] == "down") %>
<%= button_to '⬆ Migrate',
migrate_migration_path(id: migration[:version], database: migration[:database]),
method: :post,
class: 'button migration-action',
style: ('display: none;' if migration[:status] == "up" || migration[:phantom]) %>
</div>
</div>
</body>
</html>
================================================
FILE: app/views/actual_db_schema/phantom_migrations/index.html.erb
================================================
<!DOCTYPE html>
<html>
<head>
<title>Phantom Migrations</title>
<%= render partial: 'actual_db_schema/shared/js' %>
<%= render partial: 'actual_db_schema/shared/style' %>
</head>
<body>
<div>
<% flash.each do |key, message| %>
<div class="flash <%= key %>"><%= message %></div>
<% end %>
<h2>Phantom Migrations</h2>
<div class="top-buttons">
<%= link_to 'All Migrations', migrations_path, class: "top-button" %>
<% if phantom_migrations.present? %>
<%= button_to '⎌ Rollback all',
rollback_all_phantom_migrations_path,
method: :post,
class: 'button migration-action' %>
<% end %>
</div>
<% if phantom_migrations.present? %>
<table>
<thead>
<tr>
<th>Status</th>
<th>Migration ID</th>
<th>Name</th>
<th>Branch</th>
<th>Database</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<% phantom_migrations.each do |migration| %>
<tr class="migration-row phantom">
<td><%= migration[:status] %></td>
<td><%= migration[:version] %></td>
<td>
<div class="truncate-text" title="<%= migration[:name] %>">
<%= migration[:name] %>
</div>
</td>
<td><%= migration[:branch] %></td>
<td><%= migration[:database] %></td>
<td>
<div class='button-container'>
<%= link_to '👁 Show',
phantom_migration_path(id: migration[:version], database: migration[:database]),
class: 'button' %>
<%= button_to '⎌ Rollback',
rollback_phantom_migration_path(id: migration[:version], database: migration[:database]),
method: :post,
class: 'button migration-action' %>
</div>
</td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<p>No phantom migrations found.</p>
<% end %>
</div>
</body>
</html>
================================================
FILE: app/views/actual_db_schema/phantom_migrations/show.html.erb
================================================
<!DOCTYPE html>
<html>
<head>
<title>Phantom Migration Details</title>
<%= render partial: 'actual_db_schema/shared/js' %>
<%= render partial: 'actual_db_schema/shared/style' %>
</head>
<body>
<div>
<% flash.each do |key, message| %>
<div class="flash <%= key %>"><%= message %></div>
<% end %>
<h2>Phantom Migration <%= phantom_migration[:name] %> Details</h2>
<table>
<tbody>
<tr>
<th>Status</th>
<td><%= phantom_migration[:status] %></td>
</tr>
<tr>
<th>Migration ID</th>
<td><%= phantom_migration[:version] %></td>
</tr>
<tr>
<th>Branch</th>
<td><%= phantom_migration[:branch] %></td>
</tr>
<tr>
<th>Database</th>
<td><%= phantom_migration[:database] %></td>
</tr>
<tr>
<th>Path</th>
<td>
<%= phantom_migration[:filename] %>
<% source = phantom_migration[:source].to_s %>
<% if source.present? %>
<span class="source-badge"><%= source.upcase %></span>
<% end %>
</td>
</tr>
</tbody>
</table>
<h3>Migration Code</h3>
<div>
<pre><%= File.read(phantom_migration[:filename]) %></pre>
</div>
<div class='button-container'>
<%= link_to '← Back', phantom_migrations_path, class: 'button' %>
<%= button_to '⎌ Rollback',
rollback_phantom_migration_path(id: params[:id], database: params[:database]),
method: :post,
class: 'button migration-action' %>
</div>
</div>
</body>
</html>
================================================
FILE: app/views/actual_db_schema/schema/index.html.erb
================================================
<!DOCTYPE html>
<html>
<head>
<title>Database Schema</title>
<%= render partial: 'actual_db_schema/shared/js' %>
<%= render partial: 'actual_db_schema/shared/style' %>
</head>
<body>
<div>
<% flash.each do |key, message| %>
<div class="flash <%= key %>"><%= message %></div>
<% end %>
<h2>Database Schema</h2>
<div class="top-controls">
<div class="top-buttons">
<%= link_to 'All Migrations', migrations_path, class: "top-button" %>
</div>
<div class="top-search">
<%= form_tag schema_path, method: :get, class: "search-form" do %>
<span class="search-icon">🔍</span>
<%= text_field_tag :table, params[:table], placeholder: "Filter by table name", class: "search-input" %>
<% end %>
</div>
</div>
<div class="schema-diff">
<pre><%= raw schema_diff_html %></pre>
</div>
</div>
</body>
</html>
================================================
FILE: app/views/actual_db_schema/shared/_js.html
================================================
<script>
document.addEventListener('DOMContentLoaded', function() {
const migrationActions = document.querySelectorAll('.migration-action');
migrationActions.forEach(button => {
button.addEventListener('click', function(event) {
const confirmMessage = button.dataset.confirm;
if (confirmMessage && !confirm(confirmMessage)) {
event.preventDefault();
return;
}
const originalText = button.value;
button.value = 'Loading...';
disableButtons();
fetch(event.target.form.action, {
method: 'POST'
})
.then(response => {
if (response.ok) {
window.location.reload();
} else {
throw new Error('Network response was not ok.');
}
})
.catch(error => {
console.error('There has been a problem with your fetch operation:', error);
enableButtons();
button.value = originalText;
});
event.preventDefault();
});
});
function disableButtons() {
migrationActions.forEach(button => {
button.disabled = true;
});
}
function enableButtons() {
migrationActions.forEach(button => {
button.disabled = false;
});
}
});
</script>
================================================
FILE: app/views/actual_db_schema/shared/_style.html
================================================
<style>
body {
margin: 8px;
background-color: #fff;
color: #333;
}
body, p, td {
font-family: helvetica, verdana, arial, sans-serif;
font-size: 13px;
line-height: 18px;
}
h2 {
padding-left: 10px;
}
p {
padding-left: 10px;
}
table {
margin: 0;
border-collapse: collapse;
thead tr {
border-bottom: 2px solid #ddd;
}
tbody {
.migration-row.phantom {
background-color: #fff3f3;
}
.migration-row.normal {
background-color: #ffffff;
}
.migration-row:nth-child(odd).phantom {
background-color: #ffe6e6;
}
.migration-row:nth-child(odd).normal {
background-color: #f9f9f9;
}
}
td {
padding: 14px 30px;
}
}
.top-buttons {
margin: 8px;
display: flex;
align-items: center;
.top-button {
background-color: #ddd;
}
}
.button, .top-button {
font-weight: bold;
color: #000;
border: none;
padding: 5px 10px;
text-align: center;
text-decoration: none;
display: inline-block;
margin: 0 2px;
margin-right: 8px;
cursor: pointer;
border-radius: 4px;
transition: background-color 0.3s;
background: none;
}
.button:hover, .top-button:hover {
color: #fff;
background-color: #000;
}
.button:disabled, .button:hover:disabled {
background-color: transparent;
color: #666;
cursor: not-allowed;
}
.button-container {
display: flex;
}
pre {
background-color: #f7f7f7;
padding: 10px;
border: 1px solid #ddd;
}
.truncate-text {
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
}
.flash {
padding: 10px;
margin-bottom: 10px;
border-radius: 5px;
}
.flash.notice {
background-color: #d4edda;
color: #155724;
}
.flash.alert {
background-color: #f8d7da;
color: #721c24;
}
.container {
display: inline-block;
max-width: 100%;
}
.top-controls {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.top-search {
display: flex;
align-items: center;
justify-content: flex-end;
}
.search-form {
display: flex;
align-items: center;
}
.search-form .search-icon {
margin-right: 5px;
font-size: 16px;
}
.search-form .search-input {
padding: 5px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 13px;
width: 250px;
}
.schema-diff {
margin-left: 8px;
}
.source-badge {
display: inline-block;
margin-left: 8px;
padding: 2px 6px;
border-radius: 10px;
font-size: 11px;
font-weight: bold;
letter-spacing: 0.3px;
background-color: #e8f1ff;
color: #1d4ed8;
}
</style>
================================================
FILE: bin/console
================================================
#!/usr/bin/env ruby
# frozen_string_literal: true
require "bundler/setup"
require "actual_db_schema"
# You can add fixtures and/or initialization code here to make experimenting
# with your gem easier. You can also use a different console, if you like.
# (If you use this, don't forget to add pry to your Gemfile!)
# require "pry"
# Pry.start
require "irb"
IRB.start(__FILE__)
================================================
FILE: bin/setup
================================================
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
set -vx
bundle install
# Do any other automated setup that you need to do here
================================================
FILE: config/routes.rb
================================================
# frozen_string_literal: true
ActualDbSchema::Engine.routes.draw do
resources :migrations, only: %i[index show] do
member do
post :rollback
post :migrate
end
end
resources :phantom_migrations, only: %i[index show] do
member do
post :rollback
end
collection do
post :rollback_all
end
end
resources :broken_versions, only: %i[index] do
member do
post :delete
end
collection do
post :delete_all
end
end
get "schema", to: "schema#index", as: :schema
end
================================================
FILE: docker/mysql-init/create_secondary_db.sql
================================================
CREATE DATABASE actual_db_schema_test_secondary;
================================================
FILE: docker/postgres-init/create_secondary_db.sql
================================================
CREATE DATABASE actual_db_schema_test_secondary;
================================================
FILE: docker-compose.yml
================================================
version: '3.8'
services:
postgres:
image: postgres:14
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: actual_db_schema_test
ports:
- "5432:5432"
volumes:
- ./docker/postgres-init:/docker-entrypoint-initdb.d
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: actual_db_schema_test
ports:
- "3306:3306"
volumes:
- ./docker/mysql-init:/docker-entrypoint-initdb.d
================================================
FILE: gemfiles/rails.6.0.gemfile
================================================
# frozen_string_literal: true
# This file was generated by Appraisal
source "https://rubygems.org"
gem "activerecord", "~> 6.0.0"
gem "activesupport", "~> 6.0.0"
gem "minitest", "~> 5.0"
gem "mysql2", "~> 0.5.2"
gem "pg", "~> 1.5"
gem "rake"
gem "rubocop", "~> 1.21"
gem "sqlite3", "~> 1.4.0"
gemspec path: "../"
================================================
FILE: gemfiles/rails.6.1.gemfile
================================================
# frozen_string_literal: true
# This file was generated by Appraisal
source "https://rubygems.org"
gem "activerecord", "~> 6.1.0"
gem "activesupport", "~> 6.1.0"
gem "minitest", "~> 5.0"
gem "mysql2", "~> 0.5.2"
gem "pg", "~> 1.5"
gem "rake"
gem "rubocop", "~> 1.21"
gem "sqlite3", "~> 1.4.0"
gemspec path: "../"
================================================
FILE: gemfiles/rails.7.0.gemfile
================================================
# frozen_string_literal: true
# This file was generated by Appraisal
source "https://rubygems.org"
gem "activerecord", "~> 7.0.0"
gem "activesupport", "~> 7.0.0"
gem "minitest", "~> 5.0"
gem "mysql2", "~> 0.5.2"
gem "pg", "~> 1.5"
gem "rake"
gem "rubocop", "~> 1.21"
gem "sqlite3", "~> 1.4.0"
gemspec path: "../"
================================================
FILE: gemfiles/rails.7.1.gemfile
================================================
# frozen_string_literal: true
# This file was generated by Appraisal
source "https://rubygems.org"
gem "activerecord", "~> 7.1.0"
gem "activesupport", "~> 7.1.0"
gem "minitest", "~> 5.0"
gem "mysql2", "~> 0.5.2"
gem "pg", "~> 1.5"
gem "rake"
gem "rubocop", "~> 1.21"
gem "sqlite3", "~> 1.4.0"
gemspec path: "../"
================================================
FILE: gemfiles/rails.edge.gemfile
================================================
# frozen_string_literal: true
# This file was generated by Appraisal
source "https://rubygems.org"
gem "activerecord", ">= 7.2.0.beta"
gem "activesupport", ">= 7.2.0.beta"
gem "minitest", "~> 5.0"
gem "mysql2", "~> 0.5.2"
gem "pg", "~> 1.5"
gem "rake"
gem "rubocop", "~> 1.21"
gem "rails", ">= 7.2.0.beta"
gem "sqlite3"
gemspec path: "../"
================================================
FILE: lib/actual_db_schema/commands/base.rb
================================================
# frozen_string_literal: true
module ActualDbSchema
module Commands
# Base class for all commands
class Base
attr_reader :context
def initialize(context)
@context = context
end
def call
unless ActualDbSchema.config.fetch(:enabled, true)
raise "ActualDbSchema is disabled. Set ActualDbSchema.config[:enabled] = true to enable it."
end
call_impl
end
private
def call_impl
raise NotImplementedError
end
end
end
end
================================================
FILE: lib/actual_db_schema/commands/list.rb
================================================
# frozen_string_literal: true
module ActualDbSchema
module Commands
# Shows the list of phantom migrations
class List < Base
private
def call_impl
preambule
table
end
def indexed_phantom_migrations
@indexed_phantom_migrations ||= context.phantom_migrations.index_by { |m| m.version.to_s }
end
def preambule
puts "\nPhantom migrations\n\n"
puts "Below is a list of irrelevant migrations executed in unmerged branches."
puts "To bring your database schema up to date, the migrations marked as \"up\" should be rolled back."
puts "\ndatabase: #{ActualDbSchema.db_config[:database]}\n\n"
puts header.join(" ")
puts "-" * separator_width
end
def separator_width
header.map(&:length).sum + (header.size - 1) * 2
end
def header
@header ||=
[
"Status".center(8),
"Migration ID".ljust(14),
"Branch".ljust(branch_column_width),
"Migration File".ljust(16)
]
end
def table
context.migrations_status.each do |status, version|
line = line_for(status, version)
puts line if line
end
end
def line_for(status, version)
migration = indexed_phantom_migrations[version]
return unless migration
[
status.center(8),
version.to_s.ljust(14),
branch_for(version).ljust(branch_column_width),
migration.filename.gsub("#{Rails.root}/", "")
].join(" ")
end
def metadata
@metadata ||= ActualDbSchema::Store.instance.read
end
def branch_for(version)
metadata.fetch(version, {})[:branch] || "unknown"
end
def longest_branch_name
@longest_branch_name ||=
metadata.values.map { |v| v[:branch] }.compact.max_by(&:length) || "unknown"
end
def branch_column_width
longest_branch_name.length
end
end
end
end
================================================
FILE: lib/actual_db_schema/commands/rollback.rb
================================================
# frozen_string_literal: true
module ActualDbSchema
module Commands
# Rolls back all phantom migrations
class Rollback < Base
include ActualDbSchema::OutputFormatter
include ActionView::Helpers::TextHelper
def initialize(context, manual_mode: false)
@manual_mode = manual_mode || manual_mode_default?
super(context)
end
private
def call_impl
rolled_back = context.rollback_branches(manual_mode: @manual_mode)
return unless rolled_back || ActualDbSchema.failed.any?
ActualDbSchema.failed.empty? ? print_success : print_error
end
def print_success
puts colorize("[ActualDbSchema] All phantom migrations rolled back successfully! 🎉", :green)
end
def print_error
header_message = <<~HEADER
#{ActualDbSchema.failed.count} phantom migration(s) could not be rolled back automatically.
Try these steps to fix and move forward:
1. Ensure the migrations are reversible (define #up and #down methods or use #reversible).
2. If the migration references code or tables from another branch, restore or remove them.
3. Once fixed, run `rails db:migrate` again.
Below are the details of the problematic migrations:
HEADER
print_error_summary("#{header_message}\n#{failed_migrations_list}")
end
def failed_migrations_list
ActualDbSchema.failed.map.with_index(1) do |failed, index|
migration_details = colorize("Migration ##{index}:\n", :yellow)
migration_details += " File: #{failed.short_filename}\n"
migration_details += " Schema: #{failed.schema}\n" if failed.schema
migration_details + " Branch: #{failed.branch}\n"
end.join("\n")
end
def print_error_summary(content)
width = 100
indent = 4
gem_name = "ActualDbSchema"
puts colorize("╔═ [#{gem_name}] #{"═" * (width - gem_name.length - 5)}╗", :red)
print_wrapped_content(content, width, indent)
puts colorize("╚#{"═" * width}╝", :red)
end
def print_wrapped_content(content, width, indent)
usable_width = width - indent - 4
wrapped_content = word_wrap(content, line_width: usable_width)
wrapped_content.each_line do |line|
puts "#{" " * indent}#{line.chomp}"
end
end
def manual_mode_default?
ActualDbSchema.config[:auto_rollback_disabled]
end
end
end
end
================================================
FILE: lib/actual_db_schema/configuration.rb
================================================
# frozen_string_literal: true
module ActualDbSchema
# Manages the configuration settings for the gem.
class Configuration
attr_accessor :enabled, :auto_rollback_disabled, :ui_enabled, :git_hooks_enabled, :multi_tenant_schemas,
:console_migrations_enabled, :migrated_folder, :migrations_storage, :excluded_databases
def initialize
apply_defaults(default_settings)
end
def [](key)
public_send(key)
end
def []=(key, value)
public_send("#{key}=", value)
return unless key.to_sym == :migrations_storage && defined?(ActualDbSchema::Store)
ActualDbSchema::Store.instance.reset_adapter
end
def fetch(key, default = nil)
if respond_to?(key)
public_send(key)
else
default
end
end
private
def default_settings
{
enabled: enabled_by_default?,
auto_rollback_disabled: env_enabled?("ACTUAL_DB_SCHEMA_AUTO_ROLLBACK_DISABLED"),
ui_enabled: ui_enabled_by_default?,
git_hooks_enabled: env_enabled?("ACTUAL_DB_SCHEMA_GIT_HOOKS_ENABLED"),
multi_tenant_schemas: nil,
console_migrations_enabled: env_enabled?("ACTUAL_DB_SCHEMA_CONSOLE_MIGRATIONS_ENABLED"),
migrated_folder: ENV["ACTUAL_DB_SCHEMA_MIGRATED_FOLDER"].present?,
migrations_storage: migrations_storage_from_env,
excluded_databases: parse_excluded_databases_env
}
end
def enabled_by_default?
Rails.env.development?
end
def ui_enabled_by_default?
Rails.env.development? || env_enabled?("ACTUAL_DB_SCHEMA_UI_ENABLED")
end
def env_enabled?(key)
ENV[key].present?
end
def migrations_storage_from_env
ENV.fetch("ACTUAL_DB_SCHEMA_MIGRATIONS_STORAGE", "file").to_sym
end
def parse_excluded_databases_env
return [] unless ENV["ACTUAL_DB_SCHEMA_EXCLUDED_DATABASES"].present?
ENV["ACTUAL_DB_SCHEMA_EXCLUDED_DATABASES"]
.split(",")
.map(&:strip)
.reject(&:empty?)
.map(&:to_sym)
end
def apply_defaults(settings)
settings.each do |key, value|
instance_variable_set("@#{key}", value)
end
end
end
end
================================================
FILE: lib/actual_db_schema/console_migrations.rb
================================================
# frozen_string_literal: true
module ActualDbSchema
# Provides methods for executing schema modification commands directly in the Rails console.
module ConsoleMigrations
extend self
SCHEMA_METHODS = %i[
create_table
create_join_table
drop_table
change_table
add_column
remove_column
change_column
change_column_null
change_column_default
rename_column
add_index
remove_index
rename_index
add_timestamps
remove_timestamps
reversible
add_reference
remove_reference
add_foreign_key
remove_foreign_key
].freeze
SCHEMA_METHODS.each do |method_name|
define_method(method_name) do |*args, **kwargs, &block|
if kwargs.any?
migration_instance.public_send(method_name, *args, **kwargs, &block)
else
migration_instance.public_send(method_name, *args, &block)
end
end
end
private
def migration_instance
@migration_instance ||= Class.new(ActiveRecord::Migration[ActiveRecord::Migration.current_version]) {}.new
end
end
end
================================================
FILE: lib/actual_db_schema/engine.rb
================================================
# frozen_string_literal: true
module ActualDbSchema
# It isolates the namespace to avoid conflicts with the main application.
class Engine < ::Rails::Engine
isolate_namespace ActualDbSchema
initializer "actual_db_schema.initialize" do |app|
if ActualDbSchema.config[:ui_enabled]
app.routes.append do
mount ActualDbSchema::Engine => "/rails"
end
end
end
initializer "actual_db_schema.schema_dump_exclusions" do
ActiveSupport.on_load(:active_record) do
ActualDbSchema::Engine.apply_schema_dump_exclusions
end
end
def self.apply_schema_dump_exclusions
ignore_schema_dump_table(ActualDbSchema::Store::DbAdapter::TABLE_NAME)
ignore_schema_dump_table(ActualDbSchema::RollbackStatsRepository::TABLE_NAME)
return unless schema_dump_flags_supported?
return unless schema_dump_connection_available?
apply_structure_dump_flags(ActualDbSchema::Store::DbAdapter::TABLE_NAME)
apply_structure_dump_flags(ActualDbSchema::RollbackStatsRepository::TABLE_NAME)
end
class << self
private
def ignore_schema_dump_table(table_name)
return unless defined?(ActiveRecord::SchemaDumper)
return unless ActiveRecord::SchemaDumper.respond_to?(:ignore_tables)
ActiveRecord::SchemaDumper.ignore_tables |= [table_name]
end
def schema_dump_flags_supported?
defined?(ActiveRecord::Tasks::DatabaseTasks) &&
ActiveRecord::Tasks::DatabaseTasks.respond_to?(:structure_dump_flags)
end
# Avoid touching db config unless we explicitly use DB storage
# or a connection is already available.
def schema_dump_connection_available?
has_connection = begin
ActiveRecord::Base.connection_pool.connected?
rescue ActiveRecord::ConnectionNotDefined, ActiveRecord::ConnectionNotEstablished
false
end
ActualDbSchema.config[:migrations_storage] == :db || has_connection
end
def apply_structure_dump_flags(table_name)
flags = Array(ActiveRecord::Tasks::DatabaseTasks.structure_dump_flags)
adapter = ActualDbSchema.db_config[:adapter].to_s
database = database_name
if adapter.match?(/postgres/i)
flag = "--exclude-table=#{table_name}*"
flags << flag unless flags.include?(flag)
elsif adapter.match?(/mysql/i) && database
flag = "--ignore-table=#{database}.#{table_name}"
flags << flag unless flags.include?(flag)
end
ActiveRecord::Tasks::DatabaseTasks.structure_dump_flags = flags
end
def database_name
database = ActualDbSchema.db_config[:database]
if database.nil? && ActiveRecord::Base.respond_to?(:connection_db_config)
database = ActiveRecord::Base.connection_db_config&.database
end
database
end
end
end
end
================================================
FILE: lib/actual_db_schema/failed_migration.rb
================================================
# frozen_string_literal: true
module ActualDbSchema
FailedMigration = Struct.new(:migration, :exception, :branch, :schema, keyword_init: true) do
def filename
migration.filename
end
def short_filename
migration.filename.sub(File.join(Rails.root, "/"), "")
end
end
end
================================================
FILE: lib/actual_db_schema/git.rb
================================================
# frozen_string_literal: true
module ActualDbSchema
# Git helper
class Git
def self.current_branch
branch = `git rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
branch.empty? ? "unknown" : branch
rescue Errno::ENOENT
"unknown"
end
end
end
================================================
FILE: lib/actual_db_schema/git_hooks.rb
================================================
# frozen_string_literal: true
require "fileutils"
module ActualDbSchema
# Handles the installation of a git post-checkout hook that rolls back phantom migrations when switching branches
class GitHooks
include ActualDbSchema::OutputFormatter
POST_CHECKOUT_MARKER_START = "# >>> BEGIN ACTUAL_DB_SCHEMA"
POST_CHECKOUT_MARKER_END = "# <<< END ACTUAL_DB_SCHEMA"
POST_CHECKOUT_HOOK_ROLLBACK = <<~BASH
#{POST_CHECKOUT_MARKER_START}
# ActualDbSchema post-checkout hook (ROLLBACK)
# Runs db:rollback_branches on branch checkout.
# Check if this is a file checkout or creating a new branch
if [ "$3" == "0" ] || [ "$1" == "$2" ]; then
exit 0
fi
if [ -f ./bin/rails ]; then
if [ -n "$ACTUAL_DB_SCHEMA_GIT_HOOKS_ENABLED" ]; then
GIT_HOOKS_ENABLED="$ACTUAL_DB_SCHEMA_GIT_HOOKS_ENABLED"
else
GIT_HOOKS_ENABLED=$(./bin/rails runner "puts ActualDbSchema.config[:git_hooks_enabled]" 2>/dev/null)
fi
if [ "$GIT_HOOKS_ENABLED" == "true" ]; then
./bin/rails db:rollback_branches
fi
fi
#{POST_CHECKOUT_MARKER_END}
BASH
POST_CHECKOUT_HOOK_MIGRATE = <<~BASH
#{POST_CHECKOUT_MARKER_START}
# ActualDbSchema post-checkout hook (MIGRATE)
# Runs db:migrate on branch checkout.
# Check if this is a file checkout or creating a new branch
if [ "$3" == "0" ] || [ "$1" == "$2" ]; then
exit 0
fi
if [ -f ./bin/rails ]; then
if [ -n "$ACTUAL_DB_SCHEMA_GIT_HOOKS_ENABLED" ]; then
GIT_HOOKS_ENABLED="$ACTUAL_DB_SCHEMA_GIT_HOOKS_ENABLED"
else
GIT_HOOKS_ENABLED=$(./bin/rails runner "puts ActualDbSchema.config[:git_hooks_enabled]" 2>/dev/null)
fi
if [ "$GIT_HOOKS_ENABLED" == "true" ]; then
./bin/rails db:migrate
fi
fi
#{POST_CHECKOUT_MARKER_END}
BASH
def initialize(strategy: :rollback)
@strategy = strategy
end
def install_post_checkout_hook
return unless hooks_directory_present?
if File.exist?(hook_path)
handle_existing_hook
else
create_new_hook
end
end
private
def hook_code
@strategy == :migrate ? POST_CHECKOUT_HOOK_MIGRATE : POST_CHECKOUT_HOOK_ROLLBACK
end
def hooks_dir
@hooks_dir ||= Rails.root.join(".git", "hooks")
end
def hook_path
@hook_path ||= hooks_dir.join("post-checkout")
end
def hooks_directory_present?
return true if Dir.exist?(hooks_dir)
puts colorize("[ActualDbSchema] .git/hooks directory not found. Please ensure this is a Git repository.", :gray)
end
def handle_existing_hook
return update_hook if markers_exist?
return install_hook if safe_install?
show_manual_install_instructions
end
def create_new_hook
contents = <<~BASH
#!/usr/bin/env bash
#{hook_code}
BASH
write_hook_file(contents)
print_success
end
def markers_exist?
contents = File.read(hook_path)
contents.include?(POST_CHECKOUT_MARKER_START) && contents.include?(POST_CHECKOUT_MARKER_END)
end
def update_hook
contents = File.read(hook_path)
new_contents = replace_marker_contents(contents)
if new_contents == contents
message = "[ActualDbSchema] post-checkout git hook already contains the necessary code. Nothing to update."
puts colorize(message, :gray)
else
write_hook_file(new_contents)
puts colorize("[ActualDbSchema] post-checkout git hook updated successfully at #{hook_path}", :green)
end
end
def replace_marker_contents(contents)
contents.gsub(
/#{Regexp.quote(POST_CHECKOUT_MARKER_START)}.*#{Regexp.quote(POST_CHECKOUT_MARKER_END)}/m,
hook_code.strip
)
end
def safe_install?
puts colorize("[ActualDbSchema] A post-checkout hook already exists at #{hook_path}.", :gray)
puts "Overwrite the existing hook at #{hook_path}? [y,n] "
answer = $stdin.gets.chomp.downcase
answer.start_with?("y")
end
def install_hook
contents = File.read(hook_path)
new_contents = <<~BASH
#{contents.rstrip}
#{hook_code}
BASH
write_hook_file(new_contents)
print_success
end
def show_manual_install_instructions
puts colorize("[ActualDbSchema] You can follow these steps to manually install the hook:", :yellow)
puts <<~MSG
1. Open the existing post-checkout hook at:
#{hook_path}
2. Insert the following lines into that file (preferably at the end or in a relevant section).
Make sure you include the #{POST_CHECKOUT_MARKER_START} and #{POST_CHECKOUT_MARKER_END} lines:
#{hook_code}
3. Ensure the post-checkout file is executable:
chmod +x #{hook_path}
4. Done! Now when you switch branches, phantom migrations will be rolled back automatically (if enabled).
MSG
end
def write_hook_file(contents)
File.open(hook_path, "w") { |file| file.write(contents) }
FileUtils.chmod("+x", hook_path)
end
def print_success
puts colorize("[ActualDbSchema] post-checkout git hook installed successfully at #{hook_path}", :green)
end
end
end
================================================
FILE: lib/actual_db_schema/instrumentation.rb
================================================
# frozen_string_literal: true
module ActualDbSchema
module Instrumentation
ROLLBACK_EVENT = "rollback.actual_db_schema"
end
end
================================================
FILE: lib/actual_db_schema/migration.rb
================================================
# frozen_string_literal: true
module ActualDbSchema
# The Migration class is responsible for managing and retrieving migration information
class Migration
include Singleton
Migration = Struct.new(:status, :version, :name, :branch, :database, :filename, :phantom, :source,
keyword_init: true)
def all_phantom
migrations = []
MigrationContext.instance.each do |context|
indexed_migrations = context.phantom_migrations.index_by { |m| m.version.to_s }
context.migrations_status.each do |status, version|
migration = indexed_migrations[version]
migrations << build_migration_struct(status, migration) if should_include?(status, migration)
end
end
sort_migrations_desc(migrations)
end
def all
migrations = []
MigrationContext.instance.each do |context|
indexed_migrations = context.migrations.index_by { |m| m.version.to_s }
context.migrations_status.each do |status, version|
migration = indexed_migrations[version]
migrations << build_migration_struct(status, migration) if should_include?(status, migration)
end
end
sort_migrations_desc(migrations)
end
def find(version, database)
MigrationContext.instance.each do |context|
next unless ActualDbSchema.db_config[:database] == database
migration = find_migration_in_context(context, version)
return migration if migration
end
nil
end
def rollback(version, database)
MigrationContext.instance.each do |context|
next unless ActualDbSchema.db_config[:database] == database
if context.migrations.detect { |m| m.version.to_s == version }
context.run(:down, version.to_i)
break
end
end
end
def rollback_all
MigrationContext.instance.each(&:rollback_branches)
end
def migrate(version, database)
MigrationContext.instance.each do |context|
next unless ActualDbSchema.db_config[:database] == database
if context.migrations.detect { |m| m.version.to_s == version }
context.run(:up, version.to_i)
break
end
end
end
def broken_versions
broken = []
MigrationContext.instance.each do |context|
context.migrations_status.each do |status, version, name|
next unless name == "********** NO FILE **********"
broken << Migration.new(
status: status,
version: version.to_s,
name: name,
branch: branch_for(version),
database: ActualDbSchema.db_config[:database]
)
end
end
broken
end
def delete(version, database)
validate_broken_migration(version, database)
MigrationContext.instance.each do
next if database && ActualDbSchema.db_config[:database] != database
next if ActiveRecord::Base.connection.select_values("SELECT version FROM schema_migrations").exclude?(version)
ActiveRecord::Base.connection.execute("DELETE FROM schema_migrations WHERE version = '#{version}'")
break
end
end
def delete_all
broken_versions.each do |version|
delete(version.version, version.database)
end
end
private
def build_migration_struct(status, migration)
Migration.new(
status: status,
version: migration.version.to_s,
name: migration.name,
branch: branch_for(migration.version),
database: ActualDbSchema.db_config[:database],
filename: migration.filename,
phantom: phantom?(migration),
source: ActualDbSchema::Store.instance.source_for(migration.version)
)
end
def sort_migrations_desc(migrations)
migrations.sort_by { |migration| migration[:version].to_i }.reverse if migrations.any?
end
def phantom?(migration)
ActualDbSchema::Store.instance.stored_migration?(migration.filename)
end
def should_include?(status, migration)
migration && (status == "up" || !phantom?(migration))
end
def find_migration_in_context(context, version)
migration = context.migrations.detect { |m| m.version.to_s == version }
return unless migration
status = context.migrations_status.detect { |_s, v| v.to_s == version }&.first || "unknown"
build_migration_struct(status, migration)
end
def branch_for(version)
metadata.fetch(version.to_s, {})[:branch] || "unknown"
end
def metadata
@metadata ||= {}
@metadata[ActualDbSchema.db_config[:database]] ||= ActualDbSchema::Store.instance.read
end
def validate_broken_migration(version, database)
if database
unless broken_versions.any? { |v| v.version == version && v.database == database }
raise StandardError, "Migration is not broken for database #{database}."
end
else
raise StandardError, "Migration is not broken." unless broken_versions.any? { |v| v.version == version }
end
end
end
end
================================================
FILE: lib/actual_db_schema/migration_context.rb
================================================
# frozen_string_literal: true
module ActualDbSchema
# The class manages connections to each database and provides the appropriate migration context for each connection.
class MigrationContext
include Singleton
def each
original_config = current_config
configs.each do |db_config|
establish_connection(db_config)
yield context
end
ensure
establish_connection(original_config) if original_config
end
private
def establish_connection(db_config)
config = db_config.respond_to?(:config) ? db_config.config : db_config
ActiveRecord::Base.establish_connection(config)
end
def current_config
if ActiveRecord::Base.respond_to?(:connection_db_config)
ActiveRecord::Base.connection_db_config
else
ActiveRecord::Base.connection_config
end
end
def configs
all_configs = if ActiveRecord::Base.configurations.is_a?(Hash)
# Rails < 6.0 has a Hash in configurations
[ActiveRecord::Base.configurations[ActiveRecord::Tasks::DatabaseTasks.env]]
else
ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env)
end
filter_configs(all_configs)
end
def filter_configs(all_configs)
all_configs.reject do |db_config|
# Skip if database is in the excluded list
# Rails 6.0 uses spec_name, Rails 6.1+ uses name
db_name = if db_config.respond_to?(:name)
db_config.name.to_sym
elsif db_config.respond_to?(:spec_name)
db_config.spec_name.to_sym
else
:primary
end
ActualDbSchema.config.excluded_databases.include?(db_name)
end
end
def context
ar_version = Gem::Version.new(ActiveRecord::VERSION::STRING)
context = if ar_version >= Gem::Version.new("7.2.0") ||
(ar_version >= Gem::Version.new("7.1.0") && ar_version.prerelease?)
ActiveRecord::Base.connection_pool.migration_context
else
ActiveRecord::Base.connection.migration_context
end
context.extend(ActualDbSchema::Patches::MigrationContext)
end
end
end
================================================
FILE: lib/actual_db_schema/migration_parser.rb
================================================
# frozen_string_literal: true
require "ast"
require "prism"
module ActualDbSchema
# Parses migration files in a Rails application into a structured hash representation.
module MigrationParser
extend self
PARSER_MAPPING = {
add_column: ->(args) { parse_add_column(args) },
change_column: ->(args) { parse_change_column(args) },
remove_column: ->(args) { parse_remove_column(args) },
rename_column: ->(args) { parse_rename_column(args) },
add_index: ->(args) { parse_add_index(args) },
remove_index: ->(args) { parse_remove_index(args) },
rename_index: ->(args) { parse_rename_index(args) },
create_table: ->(args) { parse_create_table(args) },
drop_table: ->(args) { parse_drop_table(args) }
}.freeze
def parse_all_migrations(dirs)
changes_by_path = {}
handled_files = Set.new
dirs.each do |dir|
Dir["#{dir}/*.rb"].sort.each do |file|
base_name = File.basename(file)
next if handled_files.include?(base_name)
changes = parse_file(file).yield_self { |ast| find_migration_changes(ast) }
changes_by_path[file] = changes unless changes.empty?
handled_files.add(base_name)
end
end
changes_by_path
end
private
def parse_file(file_path)
Prism::Translation::Parser.parse_file(file_path)
end
def find_migration_changes(node)
return [] unless node.is_a?(Parser::AST::Node)
changes = []
if node.type == :block
return process_block_node(node)
elsif node.type == :send
changes.concat(process_send_node(node))
end
node.children.each { |child| changes.concat(find_migration_changes(child)) if child.is_a?(Parser::AST::Node) }
changes
end
def process_block_node(node)
changes = []
send_node = node.children.first
return changes unless send_node.type == :send
method_name = send_node.children[1]
return changes unless method_name == :create_table
change = parse_create_table_with_block(send_node, node)
changes << change if change
changes
end
def process_send_node(node)
changes = []
_receiver, method_name, *args = node.children
if (parser = PARSER_MAPPING[method_name])
change = parser.call(args)
changes << change if change
end
changes
end
def parse_add_column(args)
return unless args.size >= 3
{
action: :add_column,
table: sym_value(args[0]),
column: sym_value(args[1]),
type: sym_value(args[2]),
options: parse_hash(args[3])
}
end
def parse_change_column(args)
return unless args.size >= 3
{
action: :change_column,
table: sym_value(args[0]),
column: sym_value(args[1]),
type: sym_value(args[2]),
options: parse_hash(args[3])
}
end
def parse_remove_column(args)
return unless args.size >= 2
{
action: :remove_column,
table: sym_value(args[0]),
column: sym_value(args[1]),
options: parse_hash(args[2])
}
end
def parse_rename_column(args)
return unless args.size >= 3
{
action: :rename_column,
table: sym_value(args[0]),
old_column: sym_value(args[1]),
new_column: sym_value(args[2])
}
end
def parse_add_index(args)
return unless args.size >= 2
{
action: :add_index,
table: sym_value(args[0]),
columns: array_or_single_value(args[1]),
options: parse_hash(args[2])
}
end
def parse_remove_index(args)
return unless args.size >= 1
{
action: :remove_index,
table: sym_value(args[0]),
options: parse_hash(args[1])
}
end
def parse_rename_index(args)
return unless args.size >= 3
{
action: :rename_index,
table: sym_value(args[0]),
old_name: node_value(args[1]),
new_name: node_value(args[2])
}
end
def parse_create_table(args)
return unless args.size >= 1
{
action: :create_table,
table: sym_value(args[0]),
options: parse_hash(args[1])
}
end
def parse_drop_table(args)
return unless args.size >= 1
{
action: :drop_table,
table: sym_value(args[0]),
options: parse_hash(args[1])
}
end
def parse_create_table_with_block(send_node, block_node)
args = send_node.children[2..]
columns = parse_create_table_columns(block_node.children[2])
{
action: :create_table,
table: sym_value(args[0]),
options: parse_hash(args[1]),
columns: columns
}
end
def parse_create_table_columns(body_node)
return [] unless body_node
nodes = body_node.type == :begin ? body_node.children : [body_node]
nodes.map { |node| parse_column_node(node) }.compact
end
def parse_column_node(node)
return unless node.is_a?(Parser::AST::Node) && node.type == :send
method = node.children[1]
return parse_timestamps if method == :timestamps
{
column: sym_value(node.children[2]),
type: method,
options: parse_hash(node.children[3])
}
end
def parse_timestamps
[
{ column: :created_at, type: :datetime, options: { null: false } },
{ column: :updated_at, type: :datetime, options: { null: false } }
]
end
def sym_value(node)
return nil unless node && node.type == :sym
node.children.first
end
def array_or_single_value(node)
return [] unless node
if node.type == :array
node.children.map { |child| node_value(child) }
else
node_value(node)
end
end
def parse_hash(node)
return {} unless node && node.type == :hash
node.children.each_with_object({}) do |pair_node, result|
key_node, value_node = pair_node.children
key = sym_value(key_node) || node_value(key_node)
value = node_value(value_node)
result[key] = value
end
end
def node_value(node)
return nil unless node
case node.type
when :str, :sym, :int then node.children.first
when true then true
when false then false
when nil then nil
else
node.children.first
end
end
end
end
================================================
FILE: lib/actual_db_schema/multi_tenant.rb
================================================
# frozen_string_literal: true
module ActualDbSchema
# Handles multi-tenancy support by switching schemas for supported databases
module MultiTenant
include ActualDbSchema::OutputFormatter
class << self
def with_schema(schema_name)
context = switch_schema(schema_name)
yield
ensure
restore_context(context)
end
private
def adapter_name
ActiveRecord::Base.connection.adapter_name
end
def switch_schema(schema_name)
case adapter_name
when /postgresql/i
switch_postgresql_schema(schema_name)
when /mysql/i
switch_mysql_schema(schema_name)
else
message = "[ActualDbSchema] Multi-tenancy not supported for adapter: #{adapter_name}. " \
"Proceeding without schema switching."
puts colorize(message, :gray)
end
end
def switch_postgresql_schema(schema_name)
old_search_path = ActiveRecord::Base.connection.schema_search_path
ActiveRecord::Base.connection.schema_search_path = schema_name
{ type: :postgresql, old_context: old_search_path }
end
def switch_mysql_schema(schema_name)
old_db = ActiveRecord::Base.connection.current_database
ActiveRecord::Base.connection.execute("USE #{ActiveRecord::Base.connection.quote_table_name(schema_name)}")
{ type: :mysql, old_context: old_db }
end
def restore_context(context)
return unless context
case context[:type]
when :postgresql
ActiveRecord::Base.connection.schema_search_path = context[:old_context] if context[:old_context]
when :mysql
return unless context[:old_context]
ActiveRecord::Base.connection.execute(
"USE #{ActiveRecord::Base.connection.quote_table_name(context[:old_context])}"
)
end
end
end
end
end
================================================
FILE: lib/actual_db_schema/output_formatter.rb
================================================
# frozen_string_literal: true
module ActualDbSchema
# Provides functionality for formatting terminal output with colors
module OutputFormatter
UNICODE_COLORS = {
red: 31,
green: 32,
yellow: 33,
gray: 90
}.freeze
def colorize(text, color)
code = UNICODE_COLORS.fetch(color, 37)
"\e[#{code}m#{text}\e[0m"
end
end
end
================================================
FILE: lib/actual_db_schema/patches/migration_context.rb
================================================
# frozen_string_literal: true
module ActualDbSchema
module Patches
# Add new command to roll back the phantom migrations
module MigrationContext
include ActualDbSchema::OutputFormatter
def rollback_branches(manual_mode: false)
schemas = multi_tenant_schemas&.call || []
schema_count = schemas.any? ? schemas.size : 1
rolled_back_migrations = if schemas.any?
rollback_multi_tenant(schemas, manual_mode: manual_mode)
else
rollback_branches_for_schema(manual_mode: manual_mode)
end
delete_migrations(rolled_back_migrations, schema_count)
rolled_back_migrations.any?
end
def phantom_migrations
paths = Array(migrations_paths)
current_branch_files = Dir[*paths.flat_map { |path| "#{path}/**/[0-9]*_*.rb" }]
current_branch_file_names = current_branch_files.map { |f| ActualDbSchema.migration_filename(f) }
migrations.reject do |migration|
current_branch_file_names.include?(ActualDbSchema.migration_filename(migration.filename))
end
end
private
def rollback_branches_for_schema(manual_mode: false, schema_name: nil, rolled_back_migrations: [])
phantom_migrations.reverse_each do |migration|
next unless status_up?(migration)
show_info_for(migration, schema_name) if manual_mode
if !manual_mode || user_wants_rollback?
migrate(migration, rolled_back_migrations, schema_name,
manual_mode: manual_mode)
end
rescue StandardError => e
handle_rollback_error(migration, e, schema_name)
end
rolled_back_migrations
end
def rollback_multi_tenant(schemas, manual_mode: false)
all_rolled_back_migrations = []
schemas.each do |schema_name|
ActualDbSchema::MultiTenant.with_schema(schema_name) do
rollback_branches_for_schema(manual_mode: manual_mode, schema_name: schema_name,
rolled_back_migrations: all_rolled_back_migrations)
end
end
all_rolled_back_migrations
end
def down_migrator_for(migration)
if ActiveRecord::Migration.current_version < 6
ActiveRecord::Migrator.new(:down, [migration], migration.version)
elsif ActiveRecord::Migration.current_version < 7.1
ActiveRecord::Migrator.new(:down, [migration], schema_migration, migration.version)
else
ActiveRecord::Migrator.new(:down, [migration], schema_migration, internal_metadata, migration.version)
end
end
def migration_files
paths = Array(migrations_paths)
current_branch_files = Dir[*paths.flat_map { |path| "#{path}/**/[0-9]*_*.rb" }]
other_branches_files = ActualDbSchema::Store.instance.migration_files
current_branch_versions = current_branch_files.map { |file| file.match(/(\d+)_/)[1] }
filtered_other_branches_files = other_branches_files.reject do |file|
version = file.match(/(\d+)_/)[1]
current_branch_versions.include?(version)
end
current_branch_files + filtered_other_branches_files
end
def status_up?(migration)
migrations_status.any? do |status, version|
status == "up" && version.to_s == migration.version.to_s
end
end
def user_wants_rollback?
print "\nRollback this migration? [y,n] "
answer = $stdin.gets.chomp.downcase
answer[0] == "y"
end
def show_info_for(migration, schema_name = nil)
puts colorize("\n[ActualDbSchema] A phantom migration was found and is about to be rolled back.", :gray)
puts "Please make a decision from the options below to proceed.\n\n"
puts "Schema: #{schema_name}" if schema_name
puts "Branch: #{branch_for(migration.version.to_s)}"
puts "Database: #{ActualDbSchema.db_config[:database]}"
puts "Version: #{migration.version}\n\n"
puts File.read(migration.filename)
end
def migrate(migration, rolled_back_migrations, schema_name = nil, manual_mode: false)
migration.name = extract_class_name(migration.filename)
branch = branch_for(migration.version.to_s)
message = "[ActualDbSchema]"
message += " #{schema_name}:" if schema_name
message += " Rolling back phantom migration #{migration.version} #{migration.name} " \
"(from branch: #{branch})"
puts colorize(message, :gray)
migrator = down_migrator_for(migration)
migrator.extend(ActualDbSchema::Patches::Migrator)
migrator.migrate
notify_rollback_migration(migration: migration, schema_name: schema_name, branch: branch,
manual_mode: manual_mode)
rolled_back_migrations << migration
end
def notify_rollback_migration(migration:, schema_name:, branch:, manual_mode:)
ActiveSupport::Notifications.instrument(
ActualDbSchema::Instrumentation::ROLLBACK_EVENT,
version: migration.version.to_s,
name: migration.name,
database: ActualDbSchema.db_config[:database],
schema: schema_name,
branch: branch,
manual_mode: manual_mode
)
end
def extract_class_name(filename)
content = File.read(filename)
content.match(/^class\s+([A-Za-z0-9_]+)\s+</)[1]
end
def branch_for(version)
metadata.fetch(version, {})[:branch] || "unknown"
end
def metadata
@metadata ||= ActualDbSchema::Store.instance.read
end
def handle_rollback_error(migration, exception, schema_name = nil)
error_message = <<~ERROR
Error encountered during rollback:
#{cleaned_exception_message(exception.message)}
ERROR
puts colorize(error_message, :red)
ActualDbSchema.failed << FailedMigration.new(
migration: migration,
exception: exception,
branch: branch_for(migration.version.to_s),
schema: schema_name
)
end
def cleaned_exception_message(message)
patterns_to_remove = [
/^An error has occurred, all later migrations canceled:\s*/,
/^An error has occurred, this and all later migrations canceled:\s*/
]
patterns_to_remove.reduce(message.strip) { |msg, pattern| msg.gsub(pattern, "").strip }
end
def delete_migrations(migrations, schema_count)
migration_counts = migrations.each_with_object(Hash.new(0)) do |migration, hash|
hash[migration.filename] += 1
end
migrations.uniq.each do |migration|
count = migration_counts[migration.filename]
ActualDbSchema::Store.instance.delete(migration.filename) if count == schema_count
end
end
def multi_tenant_schemas
ActualDbSchema.config[:multi_tenant_schemas]
end
end
end
end
================================================
FILE: lib/actual_db_schema/patches/migration_proxy.rb
================================================
# frozen_string_literal: true
module ActualDbSchema
module Patches
# Records the migration file into the tmp folder after it's been migrated
module MigrationProxy
def migrate(direction)
super(direction)
ActualDbSchema::Store.instance.write(filename) if direction == :up
end
end
end
end
================================================
FILE: lib/actual_db_schema/patches/migrator.rb
================================================
# frozen_string_literal: true
module ActualDbSchema
module Patches
# Run only one migration that's being rolled back
module Migrator
def runnable
migration = migrations.first # there is only one migration, because we pass only one here
ran?(migration) ? [migration] : []
end
end
end
end
================================================
FILE: lib/actual_db_schema/railtie.rb
================================================
# frozen_string_literal: true
module ActualDbSchema
# Integrates the ConsoleMigrations module into the Rails console.
class Railtie < ::Rails::Railtie
console do
require_relative "console_migrations"
if ActualDbSchema.config[:console_migrations_enabled]
TOPLEVEL_BINDING.receiver.extend(ActualDbSchema::ConsoleMigrations)
puts "[ActualDbSchema] ConsoleMigrations enabled. You can now use migration methods directly at the console."
end
end
end
end
================================================
FILE: lib/actual_db_schema/rollback_stats_repository.rb
================================================
# frozen_string_literal: true
module ActualDbSchema
# Persists rollback events in DB.
class RollbackStatsRepository
TABLE_NAME = "actual_db_schema_rollback_events"
class << self
def record(payload)
ensure_table!
connection.execute(<<~SQL.squish)
INSERT INTO #{quoted_table}
(#{quoted_column("version")}, #{quoted_column("name")}, #{quoted_column("database")},
#{quoted_column("schema")}, #{quoted_column("branch")}, #{quoted_column("manual_mode")},
#{quoted_column("created_at")})
VALUES
(#{connection.quote(payload[:version].to_s)}, #{connection.quote(payload[:name].to_s)},
#{connection.quote(payload[:database].to_s)}, #{connection.quote((payload[:schema] || "default").to_s)},
#{connection.quote(payload[:branch].to_s)}, #{connection.quote(!!payload[:manual_mode])},
#{connection.quote(Time.current)})
SQL
end
def stats
return empty_stats unless table_exists?
{
total: total_rollbacks,
by_database: aggregate_by(:database),
by_schema: aggregate_by(:schema),
by_branch: aggregate_by(:branch)
}
end
def total_rollbacks
return 0 unless table_exists?
connection.select_value(<<~SQL.squish).to_i
SELECT COUNT(*) FROM #{quoted_table}
SQL
end
def reset!
return unless table_exists?
connection.execute("DELETE FROM #{quoted_table}")
end
private
def ensure_table!
return if table_exists?
connection.create_table(TABLE_NAME) do |t|
t.string :version, null: false
t.string :name
t.string :database, null: false
t.string :schema
t.string :branch, null: false
t.boolean :manual_mode, null: false, default: false
t.datetime :created_at, null: false
end
end
def table_exists?
connection.table_exists?(TABLE_NAME)
end
def aggregate_by(column)
return {} unless table_exists?
rows = connection.select_all(<<~SQL.squish)
SELECT #{quoted_column(column)}, COUNT(*) AS cnt
FROM #{quoted_table}
GROUP BY #{quoted_column(column)}
SQL
rows.each_with_object(Hash.new(0)) { |row, h| h[row[column.to_s].to_s] = row["cnt"].to_i }
end
def empty_stats
{
total: 0,
by_database: {},
by_schema: {},
by_branch: {}
}
end
def connection
ActiveRecord::Base.connection
end
def quoted_table
connection.quote_table_name(TABLE_NAME)
end
def quoted_column(name)
connection.quote_column_name(name)
end
end
end
end
================================================
FILE: lib/actual_db_schema/schema_diff.rb
================================================
# frozen_string_literal: true
require "tempfile"
module ActualDbSchema
# Generates a diff of schema changes between the current schema file and the
# last committed version, annotated with the migrations responsible for each change.
class SchemaDiff
include OutputFormatter
SIGN_COLORS = {
"+" => :green,
"-" => :red
}.freeze
CHANGE_PATTERNS = {
/t\.(\w+)\s+["']([^"']+)["']/ => :column,
/t\.index\s+.*name:\s*["']([^"']+)["']/ => :index,
/create_table\s+["']([^"']+)["']/ => :table
}.freeze
SQL_CHANGE_PATTERNS = {
/CREATE (?:UNIQUE\s+)?INDEX\s+["']?([^"'\s]+)["']?\s+ON\s+([\w.]+)/i => :index,
/CREATE TABLE\s+(\S+)\s+\(/i => :table,
/CREATE SEQUENCE\s+(\S+)/i => :table,
/ALTER SEQUENCE\s+(\S+)\s+OWNED BY\s+([\w.]+)/i => :table,
/ALTER TABLE\s+ONLY\s+(\S+)\s+/i => :table
}.freeze
def initialize(schema_path, migrations_path)
@schema_path = schema_path
@migrations_path = migrations_path
end
def render
if old_schema_content.nil? || old_schema_content.strip.empty?
puts colorize("Could not retrieve old schema from git.", :red)
return
end
diff_output = generate_diff(old_schema_content, new_schema_content)
process_diff_output(diff_output)
end
private
def old_schema_content
@old_schema_content ||= begin
output = `git show HEAD:#{@schema_path} 2>&1`
$CHILD_STATUS.success? ? output : nil
end
end
def new_schema_content
@new_schema_content ||= File.read(@schema_path)
end
def parsed_old_schema
@parsed_old_schema ||= parser_class.parse_string(old_schema_content.to_s)
end
def parsed_new_schema
@parsed_new_schema ||= parser_class.parse_string(new_schema_content.to_s)
end
def parser_class
structure_sql? ? StructureSqlParser : SchemaParser
end
def structure_sql?
File.extname(@schema_path) == ".sql"
end
def migration_changes
@migration_changes ||= begin
migration_dirs = [@migrations_path] + migrated_folders
MigrationParser.parse_all_migrations(migration_dirs)
end
end
def migrated_folders
ActualDbSchema::Store.instance.materialize_all
dirs = find_migrated_folders
configured_migrated_folder = ActualDbSchema.migrated_folder
relative_migrated_folder = configured_migrated_folder.to_s.sub(%r{\A#{Regexp.escape(Rails.root.to_s)}/?}, "")
dirs << relative_migrated_folder unless dirs.include?(relative_migrated_folder)
dirs.map { |dir| dir.sub(%r{\A\./}, "") }.uniq
end
def find_migrated_folders
path_parts = Pathname.new(@migrations_path).each_filename.to_a
db_index = path_parts.index("db")
return [] unless db_index
base_path = db_index.zero? ? "." : File.join(*path_parts[0...db_index])
Dir[File.join(base_path, "tmp", "migrated*")].select do |path|
File.directory?(path) && File.basename(path).match?(/^migrated(_[a-zA-Z0-9_-]+)?$/)
end
end
def generate_diff(old_content, new_content)
Tempfile.create("old_schema") do |old_file|
Tempfile.create("new_schema") do |new_file|
old_file.write(old_content)
new_file.write(new_content)
old_file.rewind
new_file.rewind
return `diff -u #{old_file.path} #{new_file.path}`
end
end
end
def process_diff_output(diff_str)
lines = diff_str.lines
current_table = nil
result_lines = []
lines.each do |line|
if (hunk_match = line.match(/^@@\s+-(\d+),(\d+)\s+\+(\d+),(\d+)\s+@@/))
current_table = find_table_in_new_schema(hunk_match[3].to_i)
elsif (ct = line.match(/create_table\s+["']([^"']+)["']/) ||
line.match(/CREATE TABLE\s+"?([^"\s]+)"?/i) || line.match(/ALTER TABLE\s+ONLY\s+(\S+)/i))
current_table = normalize_table_name(ct[1])
end
result_lines << (%w[+ -].include?(line[0]) ? handle_diff_line(line, current_table) : line)
end
result_lines.join
end
def handle_diff_line(line, current_table)
sign = line[0]
line_content = line[1..]
color = SIGN_COLORS[sign]
action, name = detect_action_and_name(line_content, sign, current_table)
annotation = action ? find_migrations(action, current_table, name) : []
annotated_line = annotation.any? ? annotate_line(line, annotation) : line
colorize(annotated_line, color)
end
def detect_action_and_name(line_content, sign, current_table)
patterns = structure_sql? ? SQL_CHANGE_PATTERNS : CHANGE_PATTERNS
action_map = {
column: ->(md) { [guess_action(sign, current_table, md[2]), md[2]] },
index: ->(md) { [sign == "+" ? :add_index : :remove_index, md[1]] },
table: ->(_) { [sign == "+" ? :create_table : :drop_table, nil] }
}
patterns.each do |regex, kind|
next unless (md = line_content.match(regex))
action_proc = action_map[kind]
return action_proc.call(md) if action_proc
end
if structure_sql? && current_table && (md = line_content.match(/^\s*"?(\w+)"?\s+(.+?)(?:,|\s*$)/i))
return [guess_action(sign, current_table, md[1]), md[1]]
end
[nil, nil]
end
def guess_action(sign, table, col_name)
case sign
when "+"
old_table = parsed_old_schema[table] || {}
old_table[col_name].nil? ? :add_column : :change_column
when "-"
new_table = parsed_new_schema[table] || {}
new_table[col_name].nil? ? :remove_column : :change_column
end
end
def find_table_in_new_schema(new_line_number)
current_table = nil
new_schema_content.lines[0...new_line_number].each do |line|
if (match = line.match(/create_table\s+["']([^"']+)["']/) || line.match(/CREATE TABLE\s+"?([^"\s]+)"?/i))
current_table = normalize_table_name(match[1])
end
end
current_table
end
def find_migrations(action, table_name, col_or_index_name)
matches = []
migration_changes.each do |file_path, changes|
changes.each do |chg|
next unless (structure_sql? && index_action?(action)) || chg[:table].to_s == table_name.to_s
matches << file_path if migration_matches?(chg, action, col_or_index_name)
end
end
matches
end
def index_action?(action)
%i[add_index remove_index rename_index].include?(action)
end
def migration_matches?(chg, action, col_or_index_name)
return (chg[:action] == action) if col_or_index_name.nil?
matchers = {
rename_column: -> { rename_column_matches?(chg, action, col_or_index_name) },
rename_index: -> { rename_index_matches?(chg, action, col_or_index_name) },
add_index: -> { index_matches?(chg, action, col_or_index_name) },
remove_index: -> { index_matches?(chg, action, col_or_index_name) }
}
matchers.fetch(chg[:action], -> { column_matches?(chg, action, col_or_index_name) }).call
end
def rename_column_matches?(chg, action, col)
(action == :remove_column && chg[:old_column].to_s == col.to_s) ||
(action == :add_column && chg[:new_column].to_s == col.to_s)
end
def rename_index_matches?(chg, action, name)
(action == :remove_index && chg[:old_name] == name) ||
(action == :add_index && chg[:new_name] == name)
end
def index_matches?(chg, action, col_or_index_name)
return false unless chg[:action] == action
extract_migration_index_name(chg, chg[:table]) == col_or_index_name.to_s
end
def column_matches?(chg, action, col_name)
chg[:column] && chg[:column].to_s == col_name.to_s && chg[:action] == action
end
def extract_migration_index_name(chg, table_name)
return chg[:options][:name].to_s if chg[:options].is_a?(Hash) && chg[:options][:name]
return "" unless (columns = chg[:columns])
cols = columns.is_a?(Array) ? columns : [columns]
"index_#{table_name}_on_#{cols.join("_and_")}"
end
def annotate_line(line, migration_file_paths)
"#{line.chomp}#{colorize(" // #{migration_file_paths.join(", ")} //", :gray)}\n"
end
def normalize_table_name(table_name)
return table_name unless structure_sql? && table_name.include?(".")
table_name.split(".").last
end
end
end
================================================
FILE: lib/actual_db_schema/schema_diff_html.rb
================================================
# frozen_string_literal: true
module ActualDbSchema
# Generates an HTML representation of the schema diff,
# annotated with the migrations responsible for each change.
class SchemaDiffHtml < SchemaDiff
def render_html(table_filter)
return unless old_schema_content && !old_schema_content.strip.empty?
@full_diff_html ||= generate_diff_html
filter = table_filter.to_s.strip.downcase
filter.empty? ? @full_diff_html : extract_table_section(@full_diff_html, filter)
end
private
def generate_diff_html
diff_output = generate_full_diff(old_schema_content, new_schema_content)
diff_output = new_schema_content if diff_output.strip.empty?
process_diff_output_for_html(diff_output)
end
def generate_full_diff(old_content, new_content)
Tempfile.create("old_schema") do |old_file|
Tempfile.create("new_schema") do |new_file|
old_file.write(old_content)
new_file.write(new_content)
old_file.rewind
new_file.rewind
`diff -u -U 9999999 #{old_file.path} #{new_file.path}`
end
end
end
def process_diff_output_for_html(diff_str)
current_table = nil
result_lines = []
@tables = {}
table_start = nil
block_depth = 1
diff_str.lines.each do |line|
next if skip_line?(line)
current_table, table_start, block_depth =
process_table(line, current_table, table_start, result_lines.size, block_depth)
result_lines << (%w[+ -].include?(line[0]) ? handle_diff_line_html(line, current_table) : line)
end
result_lines.join
end
def skip_line?(line)
line != "---\n" && !line.match(/^--- Name/) &&
(line.start_with?("---") || line.start_with?("+++") || line.match(/^@@/))
end
def process_table(line, current_table, table_start, table_end, block_depth)
if (ct = line.match(/create_table\s+["']([^"']+)["']/) || line.match(/CREATE TABLE\s+"?([^"\s]+)"?/i))
return [normalize_table_name(ct[1]), table_end, block_depth]
end
return [current_table, table_start, block_depth] unless current_table
block_depth += line.scan(/\bdo\b/).size unless line.match(/create_table\s+["']([^"']+)["']/)
block_depth -= line.scan(/\bend\b/).size
block_depth -= line.scan(/\);\s*$/).size
if block_depth.zero?
@tables[current_table] = { start: table_start, end: table_end }
current_table = nil
block_depth = 1
end
[current_table, table_start, block_depth]
end
def handle_diff_line_html(line, current_table)
sign = line[0]
line_content = line[1..]
color = SIGN_COLORS[sign]
action, name = detect_action_and_name(line_content, sign, current_table)
annotation = action ? find_migrations(action, current_table, name) : []
annotation.any? ? annotate_line(line, annotation, color) : colorize_html(line, color)
end
def annotate_line(line, migration_file_paths, color)
links_html = migration_file_paths.map { |path| link_to_migration(path) }.join(", ")
"#{colorize_html(line.chomp, color)}#{colorize_html(" // #{links_html} //", :gray)}\n"
end
def colorize_html(text, color)
safe = ERB::Util.html_escape(text)
case color
when :green
%(<span style="color: green">#{safe}</span>)
when :red
%(<span style="color: red">#{safe}</span>)
when :gray
%(<span style="color: gray">#{text}</span>)
end
end
def link_to_migration(migration_file_path)
migration = migrations.detect { |m| File.expand_path(m.filename) == File.expand_path(migration_file_path) }
return ERB::Util.html_escape(migration_file_path) unless migration
url = "migrations/#{migration.version}?database=#{migration.database}"
"<a href=\"#{url}\">#{ERB::Util.html_escape(migration_file_path)}</a>"
end
def migrations
@migrations ||= ActualDbSchema::Migration.instance.all
end
def extract_table_section(full_diff_html, table_name)
return unless @tables[table_name]
range = @tables[table_name]
full_diff_html.lines[range[:start]..range[:end]].join
end
end
end
================================================
FILE: lib/actual_db_schema/schema_parser.rb
================================================
# frozen_string_literal: true
require "parser/ast/processor"
require "prism"
module ActualDbSchema
# Parses the content of a `schema.rb` file into a structured hash representation.
module SchemaParser
module_function
def parse_string(schema_content)
ast = Prism::Translation::Parser.parse(schema_content)
collector = SchemaCollector.new
collector.process(ast)
collector.schema
end
# Internal class used to process the AST and collect schema information.
class SchemaCollector < Parser::AST::Processor
attr_reader :schema
def initialize
super()
@schema = {}
end
def on_block(node)
send_node, _args_node, body = *node
if create_table_call?(send_node)
table_name = extract_table_name(send_node)
columns = extract_columns(body)
@schema[table_name] = columns if table_name
end
super
end
def on_send(node)
_receiver, method_name, *args = *node
if method_name == :create_table && args.any?
table_name = extract_table_name(node)
@schema[table_name] ||= {}
end
super
end
private
def create_table_call?(node)
return false unless node.is_a?(Parser::AST::Node)
_receiver, method_name, *_args = node.children
method_name == :create_table
end
def extract_table_name(send_node)
_receiver, _method_name, table_arg, *_rest = send_node.children
return unless table_arg
case table_arg.type
when :str then table_arg.children.first
when :sym then table_arg.children.first.to_s
end
end
def extract_columns(body_node)
return {} unless body_node
children = body_node.type == :begin ? body_node.children : [body_node]
columns = {}
children.each do |expr|
col = process_column_node(expr)
columns[col[:name]] = { type: col[:type], options: col[:options] } if col && col[:name]
end
columns
end
def process_column_node(node)
return unless node.is_a?(Parser::AST::Node)
return unless node.type == :send
receiver, method_name, column_node, *args = node.children
return unless receiver && receiver.type == :lvar
return { name: "timestamps", type: :timestamps, options: {} } if method_name == :timestamps
col_name = extract_column_name(column_node)
options = extract_column_options(args)
{ name: col_name, type: method_name, options: options }
end
def extract_column_name(node)
return nil unless node.is_a?(Parser::AST::Node)
case node.type
when :str then node.children.first
when :sym then node.children.first.to_s
end
end
def extract_column_options(args)
opts = {}
args.each do |arg|
next unless arg && arg.type == :hash
opts.merge!(parse_hash(arg))
end
opts
end
def parse_hash(node)
hash = {}
return hash unless node && node.type == :hash
node.children.each do |pair|
key_node, value_node = pair.children
key = extract_key(key_node)
value = extract_literal(value_node)
hash[key] = value
end
hash
end
def extract_key(node)
return unless node.is_a?(Parser::AST::Node)
case node.type
when :sym then node.children.first
when :str then node.children.first.to_sym
end
end
def extract_literal(node)
return unless node.is_a?(Parser::AST::Node)
case node.type
when :int, :str, :sym then node.children.first
when true then true
when false then false
end
end
end
end
end
================================================
FILE: lib/actual_db_schema/store.rb
================================================
# frozen_string_literal: true
module ActualDbSchema
# Stores migration sources and metadata.
class Store
include Singleton
Item = Struct.new(:version, :timestamp, :branch)
def write(filename)
adapter.write(filename)
reset_source_cache
end
def read
adapter.read
end
def migration_files
adapter.migration_files
end
def delete(filename)
adapter.delete(filename)
reset_source_cache
end
def stored_migration?(filename)
adapter.stored_migration?(filename)
end
def source_for(version)
version = version.to_s
return :db if db_versions.key?(version)
return :file if file_versions.key?(version)
:unknown
end
def materialize_all
adapter.materialize_all
end
def reset_adapter
@adapter = nil
reset_source_cache
end
private
def adapter
@adapter ||= begin
storage = ActualDbSchema.config[:migrations_storage].to_s
storage == "db" ? DbAdapter.new : FileAdapter.new
end
end
def reset_source_cache
@db_versions = nil
@file_versions = nil
end
def db_versions
@db_versions ||= begin
connection = ActiveRecord::Base.connection
return {} unless connection.table_exists?(DbAdapter::TABLE_NAME)
table = connection.quote_table_name(DbAdapter::TABLE_NAME)
connection.select_values("SELECT version FROM #{table}").each_with_object({}) do |version, acc|
acc[version.to_s] = true
end
rescue StandardError
{}
end
end
def file_versions
@file_versions ||= FileAdapter.new.read
rescue StandardError
{}
end
# Stores migrated files on the filesystem with metadata in CSV.
class FileAdapter
def write(filename)
basename = File.basename(filename)
FileUtils.mkdir_p(folder)
FileUtils.copy(filename, folder.join(basename))
record_metadata(filename)
end
def read
return {} unless File.exist?(store_file)
CSV.read(store_file).map { |line| Item.new(*line) }.index_by(&:version)
end
def migration_files
Dir["#{folder}/**/[0-9]*_*.rb"]
end
def delete(filename)
File.delete(filename) if File.exist?(filename)
end
def stored_migration?(filename)
filename.to_s.start_with?(folder.to_s)
end
def materialize_all
nil
end
private
def record_metadata(filename)
version = File.basename(filename).scan(/(\d+)_.*\.rb/).first.first
CSV.open(store_file, "a") do |csv|
csv << [
version,
Time.current.iso8601,
Git.current_branch
]
end
end
def folder
ActualDbSchema.migrated_folder
end
def store_file
folder.join("metadata.csv")
end
end
# Stores migrated files in the database.
class DbAdapter
TABLE_NAME = "actual_db_schema_migrations"
RECORD_COLUMNS = %w[version filename content branch migrated_at].freeze
def write(filename)
ensure_table!
version = extract_version(filename)
return unless version
basename = File.basename(filename)
content = File.read(filename)
upsert_record(version, basename, content, Git.current_branch, Time.current)
write_cache_file(basename, content)
end
def read
return {} unless table_exists?
rows = connection.exec_query(<<~SQL.squish)
SELECT version, migrated_at, branch
FROM #{quoted_table}
SQL
rows.map do |row|
Item.new(row["version"].to_s, row["migrated_at"], row["branch"])
end.index_by(&:version)
end
def migration_files
materialize_all
Dir["#{folder}/**/[0-9]*_*.rb"]
end
def delete(filename)
version = extract_version(filename)
return unless version
if table_exists?
connection.execute(<<~SQL.squish)
DELETE FROM #{quoted_table}
WHERE #{quoted_column("version")} = #{connection.quote(version)}
SQL
end
File.delete(filename) if File.exist?(filename)
end
def stored_migration?(filename)
filename.to_s.start_with?(folder.to_s)
end
def materialize_all
return unless table_exists?
FileUtils.mkdir_p(folder)
rows = connection.exec_query(<<~SQL.squish)
SELECT filename, content
FROM #{quoted_table}
SQL
rows.each do |row|
write_cache_file(row["filename"], row["content"])
end
end
private
def upsert_record(version, basename, content, branch, migrated_at)
attributes = record_attributes(version, basename, content, branch, migrated_at)
record_exists?(version) ? update_record(attributes) : insert_record(attributes)
end
def record_attributes(version, basename, content, branch, migrated_at)
{
version: version,
filename: basename,
content: content,
branch: branch,
migrated_at: migrated_at
}
end
def update_record(attributes)
assignments = record_columns.reject { |column| column == "version" }.map do |column|
"#{quoted_column(column)} = #{connection.quote(attributes[column.to_sym])}"
end
connection.execute(<<~SQL)
UPDATE #{quoted_table}
SET #{assignments.join(", ")}
WHERE #{quoted_column("version")} = #{connection.quote(attributes[:version])}
SQL
end
def insert_record(attributes)
columns = record_columns
values = columns.map { |column| connection.quote(attributes[column.to_sym]) }
connection.execute(<<~SQL)
INSERT INTO #{quoted_table}
(#{columns.map { |column| quoted_column(column) }.join(", ")})
VALUES
(#{values.join(", ")})
SQL
end
def record_exists?(version)
connection.select_value(<<~SQL.squish).present?
SELECT 1
FROM #{quoted_table}
WHERE #{quoted_column("version")} = #{connection.quote(version)}
LIMIT 1
SQL
end
def ensure_table!
return if table_exists?
connection.create_table(TABLE_NAME) do |t|
t.string :version, null: false
t.string :filename, null: false
t.text :content, null: false
t.string :branch
t.datetime :migrated_at, null: false
end
connection.add_index(TABLE_NAME, :version, unique: true) unless connection.index_exists?(TABLE_NAME, :version)
end
def table_exists?
connection.table_exists?(TABLE_NAME)
end
def connection
ActiveRecord::Base.connection
end
def record_columns
RECORD_COLUMNS
end
def quoted_table
connection.quote_table_name(TABLE_NAME)
end
def quoted_column(name)
connection.quote_column_name(name)
end
def folder
ActualDbSchema.migrated_folder
end
def write_cache_file(filename, content)
FileUtils.mkdir_p(folder)
path = folder.join(File.basename(filename))
return if File.exist?(path) && File.read(path) == content
File.write(path, content)
end
def extract_version(filename)
match = File.basename(filename).scan(/(\d+)_.*\.rb/).first
match&.first
end
end
end
end
================================================
FILE: lib/actual_db_schema/structure_sql_parser.rb
================================================
# frozen_string_literal: true
module ActualDbSchema
# Parses the content of a `structure.sql` file into a structured hash representation.
module StructureSqlParser
module_function
def parse_string(sql_content)
schema = {}
table_regex = /CREATE TABLE\s+(?:"?([\w.]+)"?)\s*\((.*?)\);/m
sql_content.scan(table_regex) do |table_name, columns_section|
schema[normalize_table_name(table_name)] = parse_columns(columns_section)
end
schema
end
def parse_columns(columns_section)
columns = {}
columns_section.each_line do |line|
line.strip!
next if line.empty? || line =~ /^(CONSTRAINT|PRIMARY KEY|FOREIGN KEY)/i
match = line.match(/\A"?(?<col>\w+)"?\s+(?<type>\w+)(?<size>\s*\([\d,]+\))?/i)
next unless match
col_name = match[:col]
col_type = match[:type].strip.downcase.to_sym
options = {}
columns[col_name] = { type: col_type, options: options }
end
columns
end
def normalize_table_name(table_name)
return table_name unless table_name.include?(".")
table_name.split(".").last
end
end
end
================================================
FILE: lib/actual_db_schema/version.rb
================================================
# frozen_string_literal: true
module ActualDbSchema
VERSION = "0.9.1"
end
================================================
FILE: lib/actual_db_schema.rb
================================================
# frozen_string_literal: true
require "actual_db_schema/engine"
require "active_record/migration"
require "csv"
require_relative "actual_db_schema/git"
require_relative "actual_db_schema/rollback_stats_repository"
require_relative "actual_db_schema/configuration"
require_relative "actual_db_schema/instrumentation"
require_relative "actual_db_schema/store"
require_relative "actual_db_schema/version"
require_relative "actual_db_schema/migration"
require_relative "actual_db_schema/failed_migration"
require_relative "actual_db_schema/migration_context"
require_relative "actual_db_schema/migration_parser"
require_relative "actual_db_schema/output_formatter"
require_relative "actual_db_schema/patches/migration_proxy"
require_relative "actual_db_schema/patches/migrator"
require_relative "actual_db_schema/patches/migration_context"
require_relative "actual_db_schema/git_hooks"
require_relative "actual_db_schema/multi_tenant"
require_relative "actual_db_schema/railtie"
require_relative "actual_db_schema/schema_diff"
require_relative "actual_db_schema/schema_diff_html"
require_relative "actual_db_schema/schema_parser"
require_relative "actual_db_schema/structure_sql_parser"
require_relative "actual_db_schema/commands/base"
require_relative "actual_db_schema/commands/rollback"
require_relative "actual_db_schema/commands/list"
# The main module definition
module ActualDbSchema
raise NotImplementedError, "ActualDbSchema is only supported in Rails" unless defined?(Rails)
class << self
attr_accessor :config, :failed
end
self.failed = []
self.config = Configuration.new
def self.configure
yield(config)
end
def self.migrated_folder
migrated_folders.first
end
def self.migrated_folders
return [default_migrated_folder] unless migrations_paths
Array(migrations_paths).map do |path|
if path.end_with?("db/migrate")
default_migrated_folder
else
postfix = path.split("/").last
Rails.root.join("tmp", "migrated_#{postfix}")
end
end
end
def self.default_migrated_folder
config[:migrated_folder] || Rails.root.join("tmp", "migrated")
end
def self.migrations_paths
if ActiveRecord::Base.respond_to?(:connection_db_config)
ActiveRecord::Base.connection_db_config.migrations_paths
else
ActiveRecord::Base.connection_config[:migrations_paths]
end
end
def self.db_config
if ActiveRecord::Base.respond_to?(:connection_db_config)
ActiveRecord::Base.connection_db_config.configuration_hash
else
ActiveRecord::Base.connection_config
end
end
def self.migration_filename(fullpath)
fullpath.split("/").last
end
end
ActiveRecord::MigrationProxy.prepend(ActualDbSchema::Patches::MigrationProxy)
================================================
FILE: lib/generators/actual_db_schema/templates/actual_db_schema.rb
================================================
# frozen_string_literal: true
# ActualDbSchema initializer
# Adjust the configuration as needed.
if defined?(ActualDbSchema)
ActualDbSchema.configure do |config|
# Enable the gem.
config.enabled = Rails.env.development?
# Disable automatic rollback of phantom migrations.
# config.auto_rollback_disabled = true
config.auto_rollback_disabled = ENV["ACTUAL_DB_SCHEMA_AUTO_ROLLBACK_DISABLED"].present?
# Enable the UI for managing migrations.
config.ui_enabled = Rails.env.development? || ENV["ACTUAL_DB_SCHEMA_UI_ENABLED"].present?
# Enable automatic phantom migration rollback on branch switch.
# config.git_hooks_enabled = true
git_hook_enabled_env = ENV["ACTUAL_DB_SCHEMA_GIT_HOOKS_ENABLED"]
config.git_hooks_enabled = git_hook_enabled_env.nil? ? true : git_hook_enabled_env.present?
# If your application leverages multiple schemas for multi-tenancy, define the active schemas.
# config.multi_tenant_schemas = -> { ["public", "tenant1", "tenant2"] }
# Enable console migrations.
# config.console_migrations_enabled = true
config.console_migrations_enabled = ENV["ACTUAL_DB_SCHEMA_CONSOLE_MIGRATIONS_ENABLED"].present?
# Define the migrated folder location.
# config.migrated_folder = Rails.root.join("custom", "migrated")
config.migrated_folder = Rails.root.join("tmp", "migrated")
# Choose where to store migrated files: :file or :db.
# config.migrations_storage = :db
config.migrations_storage = :file
end
# Subscribe to rollback events to persist stats (optional).
# Uncomment the following to track rollback statistics in the database:
#
# ActiveSupport::Notifications.subscribe(
# ActualDbSchema::Instrumentation::ROLLBACK_EVENT
# ) do |_name, _start, _finish, _id, payload|
# ActualDbSchema::RollbackStatsRepository.record(payload)
# end
end
================================================
FILE: lib/tasks/actual_db_schema.rake
================================================
# frozen_string_literal: true
namespace :actual_db_schema do # rubocop:disable Metrics/BlockLength
desc "Install ActualDbSchema initializer and post-checkout git hook."
task :install do
extend ActualDbSchema::OutputFormatter
initializer_path = Rails.root.join("config", "initializers", "actual_db_schema.rb")
initializer_content = File.read(
File.expand_path("../../lib/generators/actual_db_schema/templates/actual_db_schema.rb", __dir__)
)
if File.exist?(initializer_path)
puts colorize("[ActualDbSchema] An initializer already exists at #{initializer_path}.", :gray)
puts "Overwrite the existing file at #{initializer_path}? [y,n] "
answer = $stdin.gets.chomp.downcase
if answer.start_with?("y")
File.write(initializer_path, initializer_content)
puts colorize("[ActualDbSchema] Initializer updated successfully at #{initializer_path}", :green)
else
puts colorize("[ActualDbSchema] Skipped overwriting the initializer.", :yellow)
end
else
File.write(initializer_path, initializer_content)
puts colorize("[ActualDbSchema] Initializer created successfully at #{initializer_path}", :green)
end
Rake::Task["actual_db_schema:install_git_hooks"].invoke
end
desc "Install ActualDbSchema post-checkout git hook that rolls back phantom migrations when switching branches."
task :install_git_hooks do
extend ActualDbSchema::OutputFormatter
puts "Which Git hook strategy would you like to install? [1, 2, 3]"
puts " 1) Rollback phantom migrations (db:rollback_branches)"
puts " 2) Migrate up to latest (db:migrate)"
puts " 3) No hook installation (skip)"
answer = $stdin.gets.chomp
strategy =
case answer
when "1" then :rollback
when "2" then :migrate
else
:none
end
if strategy == :none
puts colorize("[ActualDbSchema] Skipping git hook installation.", :gray)
else
ActualDbSchema::GitHooks.new(strategy: strategy).install_post_checkout_hook
end
end
desc "Show the schema.rb diff annotated with the migrations that made the changes"
task :diff_schema_with_migrations, %i[schema_path migrations_path] => :environment do |_, args|
default_schema = Rails.configuration.active_record.schema_format == :sql ? "./db/structure.sql" : "./db/schema.rb"
schema_path = args[:schema_path] || default_schema
migrations_path = args[:migrations_path] || "db/migrate"
schema_diff = ActualDbSchema::SchemaDiff.new(schema_path, migrations_path)
puts schema_diff.render
end
desc "Delete broken migration versions from the database"
task :delete_broken_versions, %i[versions database] => :environment do |_, args|
extend ActualDbSchema::OutputFormatter
if args[:versions]
versions = args[:versions].split(" ").map(&:strip)
versions.each do |version|
ActualDbSchema::Migration.instance.delete(version, args[:database])
puts colorize("[ActualDbSchema] Migration #{version} was successfully deleted.", :green)
rescue StandardError => e
puts colorize("[ActualDbSchema] Error deleting version #{version}: #{e.message}", :red)
end
elsif ActualDbSchema::Migration.instance.broken_versions.empty?
puts colorize("[ActualDbSchema] No broken versions found.", :gray)
else
begin
ActualDbSchema::Migration.instance.delete_all
puts colorize("[ActualDbSchema] All broken versions were successfully deleted.", :green)
rescue StandardError => e
puts colorize("[ActualDbSchema] Error deleting all broken versions: #{e.message}", :red)
end
end
end
end
================================================
FILE: lib/tasks/db.rake
================================================
# frozen_string_literal: true
namespace :db do
desc "Rollback migrations that were run inside not a merged branch."
task rollback_branches: :load_config do
ActualDbSchema.failed = []
ActualDbSchema::MigrationContext.instance.each do |context|
ActualDbSchema::Commands::Rollback.new(context).call
end
end
namespace :rollback_branches do
desc "Manually rollback phantom migrations one by one"
task manual: :load_config do
ActualDbSchema.failed = []
ActualDbSchema::MigrationContext.instance.each do |context|
ActualDbSchema::Commands::Rollback.new(context, manual_mode: true).call
end
end
end
desc "List all phantom migrations - non-relevant migrations that were run inside not a merged branch."
task phantom_migrations: :load_config do
ActualDbSchema::MigrationContext.instance.each do |context|
ActualDbSchema::Commands::List.new(context).call
end
end
task "schema:dump" => :rollback_branches
end
================================================
FILE: lib/tasks/test.rake
================================================
# frozen_string_literal: true
namespace :test do # rubocop:disable Metrics/BlockLength
desc "Run tests with SQLite3"
task :sqlite3 do
ENV["DB_ADAPTER"] = "sqlite3"
Rake::Task["test"].invoke
Rake::Task["test"].reenable
end
desc "Run tests with PostgreSQL"
task :postgresql do
sh "docker-compose up -d postgres"
wait_for_postgres
begin
ENV["DB_ADAPTER"] = "postgresql"
Rake::Task["test"].invoke
Rake::Task["test"].reenable
ensure
sh "docker-compose down"
end
end
desc "Run tests with MySQL"
task :mysql2 do
sh "docker-compose up -d mysql"
wait_for_mysql
begin
ENV["DB_ADAPTER"] = "mysql2"
Rake::Task["test"].invoke
Rake::Task["test"].reenable
ensure
sh "docker-compose down"
end
end
desc "Run tests with all adapters (SQLite3, PostgreSQL, MySQL)"
task all: %i[sqlite3 postgresql mysql2]
def wait_for_postgres
retries = 10
begin
sh "docker-compose exec -T postgres pg_isready -U postgres"
rescue StandardError
retries -= 1
raise "PostgreSQL is not ready after several attempts." if retries < 1
sleep 2
retry
end
end
def wait_for_mysql
retries = 10
begin
sh "docker-compose exec -T mysql mysqladmin ping -h 127.0.0.1 --silent"
rescue StandardError
retries -= 1
raise "MySQL is not ready after several attempts." if retries < 1
sleep 2
retry
end
end
end
================================================
FILE: sig/actual_db_schema.rbs
================================================
module ActualDbSchema
VERSION: String
# See the writing guide of rbs: https://github.com/ruby/rbs#guides
end
================================================
FILE: test/controllers/actual_db_schema/broken_versions_controller_db_storage_test.rb
================================================
# frozen_string_literal: true
require_relative "../../test_helper"
require_relative "../../../app/controllers/actual_db_schema/broken_versions_controller"
module ActualDbSchema
class BrokenVersionsControllerDbStorageTest < ActionController::TestCase
tests ActualDbSchema::BrokenVersionsController
def setup
setup_utils
configure_storage
configure_app
routes_setup
configure_views
active_record_setup
prepare_database
end
def teardown
@utils.clear_db_storage_table(TestingState.db_config)
ActualDbSchema.config[:migrations_storage] = :file
end
def routes_setup
@routes = @app.routes
Rails.application.routes.draw do
get "/rails/broken_versions" => "actual_db_schema/broken_versions#index", as: "broken_versions"
get "/rails/migrations" => "actual_db_schema/migrations#index", as: "migrations"
post "/rails/broken_version/:id/delete" => "actual_db_schema/broken_versions#delete",
as: "delete_broken_version"
post "/rails/broken_versions/delete_all" => "actual_db_schema/broken_versions#delete_all",
as: "delete_all_broken_versions"
end
ActualDbSchema::BrokenVersionsController.include(@routes.url_helpers)
end
def active_record_setup
ActiveRecord::Base.configurations = { "test" => TestingState.db_config }
ActiveRecord::Tasks::DatabaseTasks.database_configuration = { "test" => TestingState.db_config }
end
def setup_utils
@utils = TestUtils.new
end
def configure_storage
ActualDbSchema.config[:migrations_storage] = :db
end
def configure_app
@app = Rails.application
Rails.logger = Logger.new($stdout)
end
def configure_views
ActionController::Base.view_paths = [File.expand_path("../../../app/views/", __dir__)]
end
def prepare_database
@utils.reset_database_yml(TestingState.db_config)
@utils.clear_db_storage_table(TestingState.db_config)
@utils.cleanup(TestingState.db_config)
@utils.prepare_phantom_migrations(TestingState.db_config)
end
def delete_migrations_files
delete_primary_migrations
delete_secondary_migrations
end
def delete_primary_migrations
@utils.delete_migrations_files_for("tmp/migrated")
ActiveRecord::Base.establish_connection(TestingState.db_config["primary"])
[
"tmp/migrated/20130906111511_first_primary.rb",
"tmp/migrated/20130906111512_second_primary.rb"
].each do |path|
ActualDbSchema::Store.instance.delete(@utils.app_file(path))
end
end
def delete_secondary_migrations
@utils.delete_migrations_files_for("tmp/migrated_migrate_secondary")
ActiveRecord::Base.establish_connection(TestingState.db_config["secondary"])
[
"tmp/migrated_migrate_secondary/20130906111514_first_secondary.rb",
"tmp/migrated_migrate_secondary/20130906111515_second_secondary.rb"
].each do |path|
ActualDbSchema::Store.instance.delete(@utils.app_file(path))
end
end
test "GET #index returns a successful response" do
delete_migrations_files
get :index
assert_response :success
assert_select "table" do
assert_select "tbody" do
assert_select "tr" do
assert_select "td", text: "up"
assert_select "td", text: "20130906111511"
assert_select "td", text: @utils.branch_for("20130906111511")
assert_select "td", text: @utils.primary_database
end
assert_select "tr" do
assert_select "td", text: "up"
assert_select "td", text: "20130906111512"
assert_select "td", text: @utils.branch_for("20130906111512")
assert_select "td", text: @utils.primary_database
end
assert_select "tr" do
assert_select "td", text: "up"
assert_select "td", text: "20130906111514"
assert_select "td", text: @utils.branch_for("20130906111514")
assert_select "td", text: @utils.secondary_database
end
assert_select "tr" do
assert_select "td", text: "up"
assert_select "td", text: "20130906111515"
assert_select "td", text: @utils.branch_for("20130906111515")
assert_select "td", text: @utils.secondary_database
end
end
end
end
test "GET #index when there are no broken versions returns a not found text" do
get :index
assert_response :success
assert_select "p", text: "No broken versions found."
end
test "POST #delete removes migration entry from the schema_migrations table" do
delete_migrations_files
version = "20130906111511"
sql = "SELECT version FROM schema_migrations WHERE version = '#{version}'"
ActiveRecord::Base.establish_connection(TestingState.db_config["primary"])
assert_not_nil ActiveRecord::Base.connection.select_value(sql)
post :delete, params: { id: "20130906111511", database: @utils.primary_database }
assert_response :redirect
get :index
assert_select "table" do |table|
assert_no_match "20130906111511", table.text
end
assert_select ".flash", text: "Migration 20130906111511 was successfully deleted."
assert_nil ActiveRecord::Base.connection.select_value(sql)
end
test "POST #delete_all removes all broken migration entries from the schema_migrations table" do
delete_migrations_files
sql = "SELECT COUNT(*) FROM schema_migrations"
ActiveRecord::Base.establish_connection(TestingState.db_config["primary"])
assert_equal 2, ActiveRecord::Base.connection.select_value(sql)
ActiveRecord::Base.establish_connection(TestingState.db_config["secondary"])
assert_equal 2, ActiveRecord::Base.connection.select_value(sql)
post :delete_all
assert_response :redirect
get :index
assert_select "p", text: "No broken versions found."
ActiveRecord::Base.establish_connection(TestingState.db_config["primary"])
assert_equal 0, ActiveRecord::Base.connection.select_value(sql)
ActiveRecord::Base.establish_connection(TestingState.db_config["secondary"])
assert_equal 0, ActiveRecord::Base.connection.select_value(sql)
end
end
end
================================================
FILE: test/controllers/actual_db_schema/broken_versions_controller_test.rb
================================================
# frozen_string_literal: true
require_relative "../../test_helper"
require_relative "../../../app/controllers/actual_db_schema/broken_versions_controller"
module ActualDbSchema
class BrokenVersionsControllerTest < ActionController::TestCase
def setup
@utils = TestUtils.new
@app = Rails.application
routes_setup
Rails.logger = Logger.new($stdout)
ActionController::Base.view_paths = [File.expand_path("../../../app/views/", __dir__)]
active_record_setup
@utils.reset_database_yml(TestingState.db_config)
@utils.cleanup(TestingState.db_config)
@utils.prepare_phantom_migrations(TestingState.db_config)
end
def routes_setup
@routes = @app.routes
Rails.application.routes.draw do
get "/rails/broken_versions" => "actual_db_schema/broken_versions#index", as: "broken_versions"
get "/rails/migrations" => "actual_db_schema/migrations#index", as: "migrations"
post "/rails/broken_version/:id/delete" => "actual_db_schema/broken_versions#delete",
as: "delete_broken_version"
post "/rails/broken_versions/delete_all" => "actual_db_schema/broken_versions#delete_all",
as: "delete_all_broken_versions"
end
ActualDbSchema::BrokenVersionsController.include(@routes.url_helpers)
end
def active_record_setup
ActiveRecord::Base.configurations = { "test" => TestingState.db_config }
ActiveRecord::Tasks::DatabaseTasks.database_configuration = { "test" => TestingState.db_config }
end
def delete_migrations_files
@utils.delete_migrations_files_for("tmp/migrated")
@utils.delete_migrations_files_for("tmp/migrated_migrate_secondary")
end
test "GET #index returns a successful response" do
delete_migrations_files
get :index
assert_response :success
assert_select "table" do
assert_select "tbody" do
assert_select "tr" do
assert_select "td", text: "up"
assert_select "td", text: "20130906111511"
assert_select "td", text: @utils.branch_for("20130906111511")
assert_select "td", text: @utils.primary_database
end
assert_select "tr" do
assert_select "td", text: "up"
assert_select "td", text: "20130906111512"
assert_select "td", text: @utils.branch_for("20130906111512")
assert_select "td", text: @utils.primary_database
end
assert_select "tr" do
assert_select "td", text: "up"
assert_select "td", text: "20130906111514"
assert_select "td", text: @utils.branch_for("20130906111514")
assert_select "td", text: @utils.secondary_database
end
assert_select "tr" do
assert_select "td", text: "up"
assert_select "td", text: "20130906111515"
assert_select "td", text: @utils.branch_for("20130906111515")
assert_select "td", text: @utils.secondary_database
end
end
end
end
test "GET #index when there are no broken versions returns a not found text" do
get :index
assert_response :success
assert_select "p", text: "No broken versions found."
end
test "POST #delete removes migration entry from the schema_migrations table" do
delete_migrations_files
version = "20130906111511"
sql = "SELECT version FROM schema_migrations WHERE version = '#{version}'"
ActiveRecord::Base.establish_connection(TestingState.db_config["primary"])
assert_not_nil ActiveRecord::Base.connection.select_value(sql)
post :delete, params: { id: "20130906111511", database: @utils.primary_database }
assert_response :redirect
get :index
assert_select "table" do |table|
assert_no_match "20130906111511", table.text
end
assert_select ".flash", text: "Migration 20130906111511 was successfully deleted."
assert_nil ActiveRecord::Base.connection.select_value(sql)
end
test "POST #delete_all removes all broken migration entries from the schema_migrations table" do
delete_migrations_files
sql = "SELECT COUNT(*) FROM schema_migrations"
ActiveRecord::Base.establish_connection(TestingState.db_config["primary"])
assert_equal 2, ActiveRecord::Base.connection.select_value(sql)
ActiveRecord::Base.establish_connection(TestingState.db_config["secondary"])
assert_equal 2, ActiveRecord::Base.connection.select_value(sql)
post :delete_all
assert_response :redirect
get :index
assert_select "p", text: "No broken versions found."
ActiveRecord::Base.establish_connection(TestingState.db_config["primary"])
assert_equal 0, ActiveRecord::Base.connection.select_value(sql)
ActiveRecord::Base.establish_connection(TestingState.db_config["secondary"])
assert_equal 0, ActiveRecord::Base.connection.select_value(sql)
end
end
end
================================================
FILE: test/controllers/actual_db_schema/migrations_controller_db_storage_test.rb
================================================
# frozen_string_literal: true
require_relative "../../test_helper"
require_relative "../../../app/controllers/actual_db_schema/migrations_controller"
module ActualDbSchema
class MigrationsControllerDbStorageTest < ActionController::TestCase
tests ActualDbSchema::MigrationsController
def setup
setup_utils
configure_storage
configure_app
routes_setup
configure_views
active_record_setup
prepare_database
end
def teardown
@utils.clear_db_storage_table(TestingState.db_config)
ActualDbSchema.config[:migrations_storage] = :file
end
def routes_setup
@routes = @app.routes
Rails.application.routes.draw do
get "/rails/phantom_migrations" => "actual_db_schema/phantom_migrations#index", as: "phantom_migrations"
get "/rails/broken_versions" => "actual_db_schema/broken_versions#index", as: "broken_versions"
get "/rails/schema" => "actual_db_schema/schema#index", as: "schema"
get "/rails/migrations" => "actual_db_schema/migrations#index", as: "migrations"
get "/rails/migration/:id" => "actual_db_schema/migrations#show", as: "migration"
post "/rails/migration/:id/rollback" => "actual_db_schema/migrations#rollback", as: "rollback_migration"
post "/rails/migration/:id/migrate" => "actual_db_schema/migrations#migrate", as: "migrate_migration"
end
ActualDbSchema::MigrationsController.include(@routes.url_helpers)
end
def active_record_setup
ActiveRecord::Base.configurations = { "test" => TestingState.db_config }
ActiveRecord::Tasks::DatabaseTasks.database_configuration = { "test" => TestingState.db_config }
end
def setup_utils
@utils = TestUtils.new
end
def configure_storage
ActualDbSchema.config[:migrations_storage] = :db
end
def configure_app
@app = Rails.application
Rails.logger = Logger.new($stdout)
end
def configure_views
ActionController::Base.view_paths = [File.expand_path("../../../app/views/", __dir__)]
end
def prepare_database
@utils.reset_database_yml(TestingState.db_config)
@utils.clear_db_storage_table(TestingState.db_config)
@utils.cleanup(TestingState.db_config)
@utils.prepare_phantom_migrations(TestingState.db_config)
end
test "GET #index returns a successful response" do
get :index
assert_response :success
assert_select "table" do
assert_select "tbody" do
assert_select "tr" do
assert_select "td", text: "up"
assert_select "td", text: "20130906111511"
assert_select "td", text: "FirstPrimary"
assert_select "td", text: @utils.branch_for("20130906111511")
assert_select "td", text: @utils.primary_database
end
assert_select "tr" do
assert_select "td", text: "up"
assert_select "td", text: "20130906111512"
assert_select "td", text: "SecondPrimary"
assert_select "td", text: @utils.branch_for("20130906111512")
assert_select "td", text: @utils.primary_database
end
assert_select "tr" do
assert_select "td", text: "up"
assert_select "td", text: "20130906111514"
assert_select "td", text: "FirstSecondary"
assert_select "td", text: @utils.branch_for("20130906111514")
assert_select "td", text: @utils.secondary_database
end
assert_select "tr" do
assert_select "td", text: "up"
assert_select "td", text: "20130906111515"
assert_select "td", text: "SecondSecondary"
assert_select "td", text: @utils.branch_for("20130906111515")
assert_select "td", text: @utils.secondary_database
end
end
end
end
test "GET #index with search query returns filtered results" do
get :index, params: { query: "primary" }
assert_response :success
assert_select "table" do
assert_select "tbody" do
assert_select "tr" do
assert_select "td", text: "up"
assert_select "td", text: "20130906111511"
assert_select "td", text: "FirstPrimary"
assert_select "td", text: @utils.branch_for("20130906111511")
assert_select "td", text: @utils.primary_database
end
assert_select "tr" do
assert_select "td", text: "up"
assert_select "td", text: "20130906111512"
assert_select "td", text: "SecondPrimary"
assert_select "td", text: @utils.branch_for("20130906111512")
assert_select "td", text: @utils.primary_database
end
end
end
assert_no_match "20130906111514", @response.body
assert_no_match "20130906111515", @response.body
end
test "GET #show returns a successful response" do
get :show, params: { id: "20130906111511", database: @utils.primary_database }
assert_response :success
assert_select "h2", text: "Migration FirstPrimary Details"
assert_select "table" do
assert_select "tr" do
assert_select "th", text: "Status"
assert_select "td", text: "up"
end
assert_select "tr" do
assert_select "th", text: "Migration ID"
assert_select "td", text: "20130906111511"
end
assert_select "tr" do
assert_select "th", text: "Database"
assert_select "td", text: @utils.primary_database
end
assert_select "tr" do
assert_select "th", text: "Branch"
assert_select "td", text: @utils.branch_for("20130906111511")
end
end
assert_select "span.source-badge", text: "DB"
end
test "GET #show returns a 404 response if migration not found" do
get :show, params: { id: "nil", database: @utils.primary_database }
assert_response :not_found
end
test "POST #rollback with irreversible migration returns error message" do
%w[primary secondary].each do |prefix|
@utils.define_migration_file("20130906111513_irreversible_#{prefix}.rb", <<~RUBY, prefix: prefix)
class Irreversible#{prefix.camelize} < ActiveRecord::Migration[6.0]
def up
TestingState.up << :irreversible_#{prefix}
end
def down
raise ActiveRecord::IrreversibleMigration
end
end
RUBY
end
@utils.prepare_phantom_migrations(TestingState.db_config)
post :rollback, params: { id: "20130906111513", database: @utils.primary_database }
assert_response :redirect
get :index
assert_select ".flash", text: /An error has occurred/
assert_select ".flash", text: /ActiveRecord::IrreversibleMigration/
end
test "POST #rollback changes migration status to down and hide migration with down status" do
post :rollback, params: { id: "20130906111511", database: @utils.primary_database }
assert_response :redirect
get :index
assert_select "table" do
assert_select "tbody" do
assert_select "tr" do |rows|
rows.each do |row|
assert_no_match(/down/, row.text)
end
end
end
end
assert_select ".flash", text: "Migration 20130906111511 was successfully rolled back."
end
end
end
================================================
FILE: test/controllers/actual_db_schema/migrations_controller_test.rb
================================================
# frozen_string_literal: true
require_relative "../../test_helper"
require_relative "../../../app/controllers/actual_db_schema/migrations_controller"
module ActualDbSchema
class MigrationsControllerTest < ActionController::TestCase
def setup
@utils = TestUtils.new
@app = Rails.application
routes_setup
Rails.logger = Logger.new($stdout)
ActionController::Base.view_paths = [File.expand_path("../../../app/views/", __dir__)]
active_record_setup
@utils.reset_database_yml(TestingState.db_config)
@utils.cleanup(TestingState.db_config)
@utils.prepare_phantom_migrations(TestingState.db_config)
end
def routes_setup
@routes = @app.routes
Rails.application.routes.draw do
get "/rails/phantom_migrations" => "actual_db_schema/phantom_migrations#index", as: "phantom_migrations"
get "/rails/broken_versions" => "actual_db_schema/broken_versions#index", as: "broken_versions"
get "/rails/schema" => "actual_db_schema/schema#index", as: "schema"
get "/rails/migrations" => "actual_db_schema/migrations#index", as: "migrations"
get "/rails/migration/:id" => "actual_db_schema/migrations#show", as: "migration"
post "/rails/migration/:id/rollback" => "actual_db_schema/migrations#rollback", as: "rollback_migration"
post "/rails/migration/:id/migrate" => "actual_db_schema/migrations#migrate", as: "migrate_migration"
end
ActualDbSchema::MigrationsController.include(@routes.url_helpers)
end
def active_record_setup
ActiveRecord::Base.configurations = { "test" => TestingState.db_config }
ActiveRecord::Tasks::DatabaseTasks.database_configuration = { "test" => TestingState.db_config }
end
test "GET #index returns a successful response" do
get :index
assert_response :success
assert_select "table" do
assert_select "tbody" do
assert_select "tr" do
assert_select "td", text: "up"
assert_select "td", text: "20130906111511"
assert_select "td", text: "FirstPrimary"
assert_select "td", text: @utils.branch_for("20130906111511")
assert_select "td", text: @utils.primary_database
end
assert_select "tr" do
assert_select "td", text: "up"
assert_select "td", text: "20130906111512"
assert_select "td", text: "SecondPrimary"
assert_select "td", text: @utils.branch_for("20130906111512")
assert_select "td", text: @utils.primary_database
end
assert_select "tr" do
assert_select "td", text: "up"
assert_select "td", text: "20130906111514"
assert_select "td", text: "FirstSecondary"
assert_select "td", text: @utils.branch_for("20130906111514")
assert_select "td", text: @utils.secondary_database
end
assert_select "tr" do
assert_select "td", text: "up"
assert_select "td", text: "20130906111515"
assert_select "td", text: "SecondSecondary"
assert_select "td", text: @utils.branch_for("20130906111515")
assert_select "td", text: @utils.secondary_database
end
end
end
end
test "GET #index with search query returns filtered results" do
get :index, params: { query: "primary" }
assert_response :success
assert_select "table" do
assert_select "tbody" do
assert_select "tr" do
assert_select "td", text: "up"
assert_select "td", text: "20130906111511"
assert_select "td", text: "FirstPrimary"
assert_select "td", text: @utils.branch_for("20130906111511")
assert_select "td", text: @utils.primary_database
end
assert_select "tr" do
assert_select "td", text: "up"
assert_select "td", text: "20130906111512"
assert_select "td", text: "SecondPrimary"
assert_select "td", text: @utils.branch_for("20130906111512")
assert_select "td", text: @utils.primary_database
end
end
end
assert_no_match "20130906111514", @response.body
assert_no_match "20130906111515", @response.body
end
test "GET #show returns a successful response" do
get :show, params: { id: "20130906111511", database: @utils.primary_database }
assert_response :success
assert_select "h2", text: "Migration FirstPrimary Details"
assert_select "table" do
assert_select "tr" do
assert_select "th", text: "Status"
assert_select "td", text: "up"
end
assert_select "tr" do
assert_select "th", text: "Migration ID"
assert_select "td", text: "20130906111511"
end
assert_select "tr" do
assert_select "th", text: "Database"
assert_select "td", text: @utils.primary_database
end
assert_select "tr" do
assert_select "th", text: "Branch"
assert_select "td", text: @utils.branch_for("20130906111511")
end
end
assert_select "span.source-badge", text: "FILE"
end
test "GET #show returns a 404 response if migration not found" do
get :show, params: { id: "nil", database: @utils.primary_database }
assert_response :not_found
end
test "POST #rollback with irreversible migration returns error message" do
%w[primary secondary].each do |prefix|
@utils.define_migration_file("20130906111513_irreversible_#{prefix}.rb", <<~RUBY, prefix: prefix)
class Irreversible#{prefix.camelize} < ActiveRecord::Migration[6.0]
def up
TestingState.up << :irreversible_#{prefix}
end
def down
raise ActiveRecord::IrreversibleMigration
end
end
RUBY
end
@utils.prepare_phantom_migrations(TestingState.db_config)
post :rollback, params: { id: "20130906111513", database: @utils.primary_database }
assert_response :redirect
get :index
assert_select ".flash", text: /An error has occurred/
assert_select ".flash", text: /ActiveRecord::IrreversibleMigration/
end
test "POST #rollback changes migration status to down and hide migration with down status" do
post :rollback, params: { id: "20130906111511", database: @utils.primary_database }
assert_response :redirect
get :index
assert_select "table" do
assert_select "tbody" do
assert_select "tr" do |rows|
rows.each do |row|
assert_no_match(/down/, row.text)
end
end
end
end
assert_select ".flash", text: "Migration 20130906111511 was successfully rolled back."
end
end
end
================================================
FILE: test/controllers/actual_db_schema/phantom_migrations_controller_db_storage_test.rb
================================================
# frozen_string_literal: true
require_relative "../../test_helper"
require_relative "../../../app/controllers/actual_db_schema/phantom_migrations_controller"
module ActualDbSchema
class PhantomMigrationsControllerDbStorageTest < ActionController::TestCase
tests ActualDbSchema::PhantomMigrationsController
def setup
setup_utils
configure_storage
configure_app
routes_setup
configure_views
active_record_setup
prepare_database
end
def teardown
@utils.clear_db_storage_table(TestingState.db_config)
ActualDbSchema.config[:migrations_storage] = :file
end
def routes_setup
@routes = @app.routes
Rails.application.routes.draw do
get "/rails/migrations" => "actual_db_schema/migrations#index", as: "migrations"
get "/rails/phantom_migrations" => "actual_db_schema/phantom_migrations#index", as: "phantom_migrations"
get "/rails/phantom_migration/:id" => "actual_db_schema/phantom_migrations#show", as: "phantom_migration"
post "/rails/phantom_migration/:id/rollback" => "actual_db_schema/phantom_migrations#rollback",
as: "rollback_phantom_migration"
post "/rails/phantom_migrations/rollback_all" => "actual_db_schema/phantom_migrations#rollback_all",
as: "rollback_all_phantom_migrations"
end
ActualDbSchema::PhantomMigrationsController.include(@routes.url_helpers)
end
def active_record_setup
ActiveRecord::Base.configurations = { "test" => TestingState.db_config }
ActiveRecord::Tasks::DatabaseTasks.database_configuration = { "test" => TestingState.db_config }
end
def setup_utils
@utils = TestUtils.new
end
def configure_storage
ActualDbSchema.config[:migrations_storage] = :db
end
def configure_app
@app = Rails.application
Rails.logger = Logger.new($stdout)
end
def configure_views
ActionController::Base.view_paths = [File.expand_path("../../../app/views/", __dir__)]
end
def prepare_database
@utils.reset_database_yml(TestingState.db_config)
@utils.clear_db_storage_table(TestingState.db_config)
@utils.cleanup(TestingState.db_config)
@utils.prepare_phantom_migrations(TestingState.db_config)
end
test "GET #index returns a successful response" do
get :index
assert_response :success
assert_select "table" do
assert_select "tbody" do
assert_select "tr" do |rows|
rows.each do |row|
assert_no_match(/down/, row.text)
end
end
assert_select "tr" do
assert_select "td", text: "up"
assert_select "td", text: "20130906111511"
assert_select "td", text: "FirstPrimary"
assert_select "td", text: @utils.branch_for("20130906111511")
assert_select "td", text: @utils.primary_database
end
assert_select "tr" do
assert_select "td", text: "up"
assert_select "td", text: "20130906111512"
assert_select "td", text: "SecondPrimary"
assert_select "td", text: @utils.branch_for("20130906111512")
assert_select "td", text: @utils.primary_database
end
assert_select "tr" do
assert_select "td", text: "up"
assert_select "td", text: "20130906111514"
assert_select "td", text: "FirstSecondary"
assert_select "td", text: @utils.branch_for("20130906111514")
assert_select "td", text: @utils.secondary_database
end
assert_select "tr" do
assert_select "td", text: "up"
assert_select "td", text: "20130906111515"
assert_select "td", text: "SecondSecondary"
assert_select "td", text: @utils.branch_for("20130906111515")
assert_select "td", text: @utils.secondary_database
end
end
end
end
test "GET #index when all migrations is down returns a not found text" do
@utils.run_migrations
get :index
assert_response :success
assert_select "p", text: "No phantom migrations found."
end
test "GET #show returns a successful response" do
get :show, params: { id: "20130906111511", database: @utils.primary_database }
assert_response :success
assert_select "h2", text: "Phantom Migration FirstPrimary Details"
assert_select "table" do
assert_select "tr" do
assert_select "th", text: "Status"
assert_select "td", text: "up"
end
assert_select "tr" do
assert_select "th", text: "Migration ID"
assert_select "td", text: "20130906111511"
end
assert_select "tr" do
assert_select "th", text: "Database"
assert_select "td", text: @utils.primary_database
end
assert_select "tr" do
assert_select "th", text: "Branch"
assert_select "td", text: @utils.branch_for("20130906111511")
end
end
assert_select "span.source-badge", text: "DB"
end
test "GET #show returns a 404 response if migration not found" do
get :show, params: { id: "nil", database: @utils.primary_database }
assert_response :not_found
end
test "POST #rollback changes migration status to down and hide migration with down status" do
post :rollback, params: { id: "20130906111511", database: @utils.primary_database }
assert_response :redirect
get :index
assert_select "table" do
assert_select "tbody" do
assert_select "tr" do |rows|
rows.each do |row|
assert_no_match(/down/, row.text)
end
end
assert_select "tr" do
assert_select "td", text: "up"
assert_select "td", text: "20130906111512"
assert_select "td", text: "SecondPrimary"
assert_select "td", text: @utils.branch_for("20130906111512")
end
end
end
assert_select ".flash", text: "Migration 20130906111511 was successfully rolled back."
end
test "POST #rollback with irreversible migration returns error message" do
%w[primary secondary].each do |prefix|
@utils.define_migration_file("20130906111513_irreversible_#{prefix}.rb", <<~RUBY, prefix: prefix)
class Irreversible#{prefix.camelize} < ActiveRecord::Migration[6.0]
def up
TestingState.up << :irreversible_#{prefix}
end
def down
raise ActiveRecord::IrreversibleMigration
end
end
RUBY
end
@utils.prepare_phantom_migrations(TestingState.db_config)
post :rollback, params: { id: "20130906111513", database: @utils.primary_database }
assert_response :redirect
get :index
assert_select ".flash", text: /An error has occurred/
assert_select ".flash", text: /ActiveRecord::IrreversibleMigration/
end
test "POST #rollback_all changes all phantom migrations status to down and hide migration with down status" do
post :rollback_all
assert_response :redirect
get :index
assert_select "p", text: "No phantom migrations found."
end
end
end
================================================
FILE: test/controllers/actual_db_schema/phantom_migrations_controller_test.rb
================================================
# frozen_string_literal: true
require_relative "../../test_helper"
require_relative "../../../app/controllers/actual_db_schema/phantom_migrations_controller"
module ActualDbSchema
class PhantomMigrationsControllerTest < ActionController::TestCase
def setup
@utils = TestUtils.new
@app = Rails.application
routes_setup
Rails.logger = Logger.new($stdout)
ActionController::Base.view_paths = [File.expand_path("../../../app/views/", __dir__)]
active_record_setup
@utils.reset_database_yml(TestingState.db_config)
@utils.cleanup(TestingState.db_config)
@utils.prepare_phantom_migrations(TestingState.db_config)
end
def routes_setup
@routes = @app.routes
Rails.application.routes.draw do
get "/rails/migrations" => "actual_db_schema/migrations#index", as: "migrations"
get "/rails/phantom_migrations" => "actual_db_schema/phantom_migrations#index", as: "phantom_migrations"
get "/rails/phantom_migration/:id" => "actual_db_schema/phantom_migrations#show", as: "phantom_migration"
post "/rails/phantom_migration/:id/rollback" => "actual_db_schema/phantom_migrations#rollback",
as: "rollback_phantom_migration"
post "/rails/phantom_migrations/rollback_all" => "actual_db_schema/phantom_migrations#rollback_all",
as: "rollback_all_phantom_migrations"
end
ActualDbSchema::PhantomMigrationsController.include(@routes.url_helpers)
end
def active_record_setup
ActiveRecord::Base.configurations = { "test" => TestingState.db_config }
ActiveRecord::Tasks::DatabaseTasks.database_configuration = { "test" => TestingState.db_config }
end
test "GET #index returns a successful response" do
get :index
assert_response :success
assert_select "table" do
assert_select "tbody" do
assert_select "tr" do |rows|
rows.each do |row|
assert_no_match(/down/, row.text)
end
end
assert_select "tr" do
assert_select "td", text: "up"
assert_select "td", text: "20130906111511"
assert_select "td", text: "FirstPrimary"
assert_select "td", text: @utils.branch_for("20130906111511")
assert_select "td", text: @utils.primary_database
end
assert_select "tr" do
assert_select "td", text: "up"
assert_select "td", text: "20130906111512"
assert_select "td", text: "SecondPrimary"
assert_select "td", text: @utils.branch_for("20130906111512")
assert_select "td", text: @utils.primary_database
end
assert_select "tr" do
assert_select "td", text: "up"
assert_select "td", text: "20130906111514"
assert_select "td", text: "FirstSecondary"
assert_select "td", text: @utils.branch_for("20130906111514")
assert_select "td", text: @utils.secondary_database
end
assert_select "tr" do
assert_select "td", text: "up"
assert_select "td", text: "20130906111515"
assert_select "td", text: "SecondSecondary"
assert_select "td", text: @utils.branch_for("20130906111515")
assert_select "td", text: @utils.secondary_database
end
end
end
end
test "GET #index when all migrations is down returns a not found text" do
@utils.run_migrations
get :index
assert_response :success
assert_select "p", text: "No phantom migrations found."
end
test "GET #show returns a successful response" do
get :show, params: { id: "20130906111511", database: @utils.primary_database }
assert_response :success
assert_select "h2", text: "Phantom Migration FirstPrimary Details"
assert_select "table" do
assert_select "tr" do
assert_select "th", text: "Status"
assert_select "td", text: "up"
end
assert_select "tr" do
assert_select "th", text: "Migration ID"
assert_select "td", text: "20130906111511"
end
assert_select "tr" do
assert_select "th", text: "Database"
assert_select "td", text: @utils.primary_database
end
assert_select "tr" do
assert_select "th", text: "Branch"
assert_select "td", text: @utils.branch_for("20130906111511")
end
end
assert_select "span.source-badge", text: "FILE"
end
test "GET #show returns a 404 response if migration not found" do
get :show, params: { id: "nil", database: @utils.primary_database }
assert_response :not_found
end
test "POST #rollback changes migration status to down and hide migration with down status" do
post :rollback, params: { id: "20130906111511", database: @utils.primary_database }
assert_response :redirect
get :index
assert_select "table" do
assert_select "tbody" do
assert_select "tr" do |rows|
rows.each do |row|
assert_no_match(/down/, row.text)
end
end
assert_select "tr" do
assert_select "td", text: "up"
assert_select "td", text: "20130906111512"
assert_select "td", text: "SecondPrimary"
assert_select "td", text: @utils.branch_for("20130906111512")
end
end
end
assert_select ".flash", text: "Migration 20130906111511 was successfully rolled back."
end
test "POST #rollback with irreversible migration returns error message" do
%w[primary secondary].each do |prefix|
@utils.define_migration_file("20130906111513_irreversible_#{prefix}.rb", <<~RUBY, prefix: prefix)
class Irreversible#{prefix.camelize} < ActiveRecord::Migration[6.0]
def up
TestingState.up << :irreversible_#{prefix}
end
def down
raise ActiveRecord::IrreversibleMigration
end
end
RUBY
end
@utils.prepare_phantom_migrations(TestingState.db_config)
post :rollback, params: { id: "20130906111513", database: @utils.primary_database }
assert_response :redirect
get :index
assert_select ".flash", text: /An error has occurred/
assert_select ".flash", text: /ActiveRecord::IrreversibleMigration/
end
test "POST #rollback_all changes all phantom migrations status to down and hide migration with down status" do
post :rollback_all
assert_response :redirect
get :index
assert_select "p", text: "No phantom migrations found."
end
end
end
================================================
FILE: test/controllers/actual_db_schema/schema_controller_db_storage_test.rb
================================================
# frozen_string_literal: true
require_relative "../../test_helper"
require_relative "../../../app/controllers/actual_db_schema/schema_controller"
module ActualDbSchema
class SchemaControllerDbStorageTest < ActionController::TestCase
tests ActualDbSchema::SchemaController
def setup
setup_utils
configure_storage
configure_app
routes_setup
configure_views
active_record_setup
prepare_database
stub_schema_diff
end
def teardown
@utils.define_migration_file("20250212084323_drop_users.rb", <<~RUBY)
class DropUsers < ActiveRecord::Migration[6.0]
def change
drop_table :users, if_exists: true
end
end
RUBY
@utils.define_migration_file("20250212084324_drop_products.rb", <<~RUBY)
class DropProducts < ActiveRecord::Migration[6.0]
def change
drop_table :products, if_exists: true
end
end
RUBY
@utils.run_migrations
@utils.clear_db_storage_table(TestingState.db_config)
ActualDbSchema.config[:migrations_storage] = :file
end
def routes_setup
@routes = @app.routes
Rails.application.routes.draw do
get "/rails/migrations" => "actual_db_schema/migrations#index", as: "migrations"
get "/rails/schema" => "actual_db_schema/schema#index", as: "schema"
end
ActualDbSchema::SchemaController.include(@routes.url_helpers)
end
def active_record_setup
ActiveRecord::Base.configurations = { "test" => TestingState.db_config }
ActiveRecord::Tasks::DatabaseTasks.database_configuration = { "test" => TestingState.db_config }
end
def setup_utils
@utils = TestUtils.new
end
def configure_storage
ActualDbSchema.config[:migrations_storage] = :db
end
def configure_app
@app = Rails.application
Rails.logger = Logger.new($stdout)
end
def configure_views
ActionController::Base.view_paths = [File.expand_path("../../../app/views/", __dir__)]
end
def prepare_database
@utils.reset_database_yml(TestingState.db_config)
@utils.clear_db_storage_table(TestingState.db_config)
@utils.cleanup(TestingState.db_config)
define_migrations
end
def stub_schema_diff
ActualDbSchema::SchemaDiffHtml.define_method(:initialize) do |_schema_path, _migrations_path|
@schema_path = "test/dummy_app/db/schema.rb"
@migrations_path = "test/dummy_app/db/migrate"
end
end
def define_migrations
@utils.define_migration_file("20250212084321_create_users_table.rb", <<~RUBY)
class CreateUsersTable < ActiveRecord::Migration[6.0]
def change
create_table :users do |t|
t.string :name
t.timestamps
end
end
end
RUBY
@utils.define_migration_file("20250212084322_create_products_table.rb", <<~RUBY)
class CreateProductsTable < ActiveRecord::Migration[6.0]
def change
create_table :products do |t|
t.string :name
t.timestamps
end
end
end
RUBY
@utils.run_migrations
ActualDbSchema::SchemaDiff.define_method(:old_schema_content) do
<<~RUBY
ActiveRecord::Schema[6.0].define(version: 20250212084322) do
create_table "products", force: :cascade do |t|
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "users", force: :cascade do |t|
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
end
RUBY
end
end
test "GET #index returns a successful response" do
file_name = "20250212084325_add_surname_to_users.rb"
@utils.define_migration_file(file_name, <<~RUBY)
class AddSurnameToUsers < ActiveRecord::Migration[6.0]
def change
add_column :users, :surname, :string
end
end
RUBY
@utils.run_migrations
get :index
assert_response :success
assert_select "h2", text: "Database Schema"
assert_select "div.schema-diff pre" do |pre|
assert_match(/create_table "products"/, pre.text)
assert_match(/create_table "users"/, pre.text)
assert_match(%r{\+ t\.string "surname" // #{File.join("test/dummy_app/db/migrate", file_name)} //}, pre.text)
end
end
test "GET #index with search query returns filtered results" do
get :index, params: { table: "users" }
assert_response :success
assert_select "h2", text: "Database Schema"
assert_select "div.schema-diff pre" do |pre|
assert_match(/create_table "users"/, pre.text)
refute_match(/create_table "products"/, pre.text)
end
end
end
end
================================================
FILE: test/controllers/actual_db_schema/schema_controller_test.rb
================================================
# frozen_string_literal: true
require_relative "../../test_helper"
require_relative "../../../app/controllers/actual_db_schema/schema_controller"
module ActualDbSchema
class SchemaControllerTest < ActionController::TestCase
def setup
@utils = TestUtils.new
@app = Rails.application
routes_setup
Rails.logger = Logger.new($stdout)
ActionController::Base.view_paths = [File.expand_path("../../../app/views/", __dir__)]
active_record_setup
@utils.reset_database_yml(TestingState.db_config)
@utils.cleanup(TestingState.db_config)
define_migrations
end
def teardown
@utils.define_migration_file("20250212084323_drop_users_table.rb", <<~RUBY)
class DropUsersTable < ActiveRecord::Migration[6.0]
def change
drop_table :users, if_exists: true
end
end
RUBY
@utils.define_migration_file("20250212084324_drop_products_table.rb", <<~RUBY)
class DropProductsTable < ActiveRecord::Migration[6.0]
def change
drop_table :products, if_exists: true
end
end
RUBY
@utils.run_migrations
end
def routes_setup
@routes = @app.routes
Rails.application.routes.draw do
get "/ra
gitextract_9537yhwc/
├── .github/
│ └── workflows/
│ └── main.yml
├── .gitignore
├── .rubocop.yml
├── Appraisals
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── actual_db_schema.gemspec
├── app/
│ ├── controllers/
│ │ └── actual_db_schema/
│ │ ├── broken_versions_controller.rb
│ │ ├── migrations_controller.rb
│ │ ├── phantom_migrations_controller.rb
│ │ └── schema_controller.rb
│ └── views/
│ └── actual_db_schema/
│ ├── broken_versions/
│ │ └── index.html.erb
│ ├── migrations/
│ │ ├── index.html.erb
│ │ └── show.html.erb
│ ├── phantom_migrations/
│ │ ├── index.html.erb
│ │ └── show.html.erb
│ ├── schema/
│ │ └── index.html.erb
│ └── shared/
│ ├── _js.html
│ └── _style.html
├── bin/
│ ├── console
│ └── setup
├── config/
│ └── routes.rb
├── docker/
│ ├── mysql-init/
│ │ └── create_secondary_db.sql
│ └── postgres-init/
│ └── create_secondary_db.sql
├── docker-compose.yml
├── gemfiles/
│ ├── rails.6.0.gemfile
│ ├── rails.6.1.gemfile
│ ├── rails.7.0.gemfile
│ ├── rails.7.1.gemfile
│ └── rails.edge.gemfile
├── lib/
│ ├── actual_db_schema/
│ │ ├── commands/
│ │ │ ├── base.rb
│ │ │ ├── list.rb
│ │ │ └── rollback.rb
│ │ ├── configuration.rb
│ │ ├── console_migrations.rb
│ │ ├── engine.rb
│ │ ├── failed_migration.rb
│ │ ├── git.rb
│ │ ├── git_hooks.rb
│ │ ├── instrumentation.rb
│ │ ├── migration.rb
│ │ ├── migration_context.rb
│ │ ├── migration_parser.rb
│ │ ├── multi_tenant.rb
│ │ ├── output_formatter.rb
│ │ ├── patches/
│ │ │ ├── migration_context.rb
│ │ │ ├── migration_proxy.rb
│ │ │ └── migrator.rb
│ │ ├── railtie.rb
│ │ ├── rollback_stats_repository.rb
│ │ ├── schema_diff.rb
│ │ ├── schema_diff_html.rb
│ │ ├── schema_parser.rb
│ │ ├── store.rb
│ │ ├── structure_sql_parser.rb
│ │ └── version.rb
│ ├── actual_db_schema.rb
│ ├── generators/
│ │ └── actual_db_schema/
│ │ └── templates/
│ │ └── actual_db_schema.rb
│ └── tasks/
│ ├── actual_db_schema.rake
│ ├── db.rake
│ └── test.rake
├── sig/
│ └── actual_db_schema.rbs
└── test/
├── controllers/
│ └── actual_db_schema/
│ ├── broken_versions_controller_db_storage_test.rb
│ ├── broken_versions_controller_test.rb
│ ├── migrations_controller_db_storage_test.rb
│ ├── migrations_controller_test.rb
│ ├── phantom_migrations_controller_db_storage_test.rb
│ ├── phantom_migrations_controller_test.rb
│ ├── schema_controller_db_storage_test.rb
│ └── schema_controller_test.rb
├── dummy_app/
│ ├── config/
│ │ └── .keep
│ ├── db/
│ │ ├── migrate/
│ │ │ └── .keep
│ │ └── migrate_secondary/
│ │ └── .keep
│ └── public/
│ └── 404.html
├── rake_task_console_migrations_db_storage_test.rb
├── rake_task_console_migrations_test.rb
├── rake_task_db_storage_full_test.rb
├── rake_task_db_storage_test.rb
├── rake_task_delete_broken_versions_db_storage_test.rb
├── rake_task_delete_broken_versions_test.rb
├── rake_task_git_hooks_install_db_storage_test.rb
├── rake_task_git_hooks_install_test.rb
├── rake_task_multi_tenant_db_storage_test.rb
├── rake_task_multi_tenant_test.rb
├── rake_task_schema_diff_db_storage_test.rb
├── rake_task_schema_diff_test.rb
├── rake_task_secondary_db_storage_test.rb
├── rake_task_secondary_test.rb
├── rake_task_test.rb
├── rake_tasks_all_databases_db_storage_test.rb
├── rake_tasks_all_databases_test.rb
├── support/
│ └── test_utils.rb
├── test_actual_db_schema.rb
├── test_actual_db_schema_db_storage_test.rb
├── test_database_filtering.rb
├── test_helper.rb
└── test_migration_context.rb
SYMBOL INDEX (499 symbols across 52 files)
FILE: app/controllers/actual_db_schema/broken_versions_controller.rb
type ActualDbSchema (line 3) | module ActualDbSchema
class BrokenVersionsController (line 5) | class BrokenVersionsController < ActionController::Base
method index (line 9) | def index; end
method delete (line 11) | def delete
method delete_all (line 16) | def delete_all
method handle_delete (line 23) | def handle_delete(id, database)
method handle_delete_all (line 30) | def handle_delete_all
method broken_versions (line 37) | def broken_versions
FILE: app/controllers/actual_db_schema/migrations_controller.rb
type ActualDbSchema (line 3) | module ActualDbSchema
class MigrationsController (line 5) | class MigrationsController < ActionController::Base
method index (line 9) | def index; end
method show (line 11) | def show
method rollback (line 15) | def rollback
method migrate (line 20) | def migrate
method handle_rollback (line 27) | def handle_rollback(id, database)
method handle_migrate (line 34) | def handle_migrate(id, database)
method migrations (line 41) | def migrations
method migration (line 59) | def migration
FILE: app/controllers/actual_db_schema/phantom_migrations_controller.rb
type ActualDbSchema (line 3) | module ActualDbSchema
class PhantomMigrationsController (line 5) | class PhantomMigrationsController < ActionController::Base
method index (line 9) | def index; end
method show (line 11) | def show
method rollback (line 15) | def rollback
method rollback_all (line 20) | def rollback_all
method handle_rollback (line 27) | def handle_rollback(id, database)
method handle_rollback_all (line 34) | def handle_rollback_all
method phantom_migrations (line 41) | def phantom_migrations
method phantom_migration (line 45) | def phantom_migration
FILE: app/controllers/actual_db_schema/schema_controller.rb
type ActualDbSchema (line 3) | module ActualDbSchema
class SchemaController (line 5) | class SchemaController < ActionController::Base
method index (line 9) | def index; end
method schema_diff_html (line 13) | def schema_diff_html
FILE: lib/actual_db_schema.rb
type ActualDbSchema (line 33) | module ActualDbSchema
function configure (line 43) | def self.configure
function migrated_folder (line 47) | def self.migrated_folder
function migrated_folders (line 51) | def self.migrated_folders
function default_migrated_folder (line 64) | def self.default_migrated_folder
function migrations_paths (line 68) | def self.migrations_paths
function db_config (line 76) | def self.db_config
function migration_filename (line 84) | def self.migration_filename(fullpath)
FILE: lib/actual_db_schema/commands/base.rb
type ActualDbSchema (line 3) | module ActualDbSchema
type Commands (line 4) | module Commands
class Base (line 6) | class Base
method initialize (line 9) | def initialize(context)
method call (line 13) | def call
method call_impl (line 23) | def call_impl
FILE: lib/actual_db_schema/commands/list.rb
type ActualDbSchema (line 3) | module ActualDbSchema
type Commands (line 4) | module Commands
class List (line 6) | class List < Base
method call_impl (line 9) | def call_impl
method indexed_phantom_migrations (line 14) | def indexed_phantom_migrations
method preambule (line 18) | def preambule
method separator_width (line 27) | def separator_width
method header (line 31) | def header
method table (line 41) | def table
method line_for (line 48) | def line_for(status, version)
method metadata (line 60) | def metadata
method branch_for (line 64) | def branch_for(version)
method longest_branch_name (line 68) | def longest_branch_name
method branch_column_width (line 73) | def branch_column_width
FILE: lib/actual_db_schema/commands/rollback.rb
type ActualDbSchema (line 3) | module ActualDbSchema
type Commands (line 4) | module Commands
class Rollback (line 6) | class Rollback < Base
method initialize (line 10) | def initialize(context, manual_mode: false)
method call_impl (line 17) | def call_impl
method print_success (line 25) | def print_success
method print_error (line 29) | def print_error
method failed_migrations_list (line 44) | def failed_migrations_list
method print_error_summary (line 53) | def print_error_summary(content)
method print_wrapped_content (line 63) | def print_wrapped_content(content, width, indent)
method manual_mode_default? (line 71) | def manual_mode_default?
FILE: lib/actual_db_schema/configuration.rb
type ActualDbSchema (line 3) | module ActualDbSchema
class Configuration (line 5) | class Configuration
method initialize (line 9) | def initialize
method [] (line 13) | def [](key)
method []= (line 17) | def []=(key, value)
method fetch (line 24) | def fetch(key, default = nil)
method default_settings (line 34) | def default_settings
method enabled_by_default? (line 48) | def enabled_by_default?
method ui_enabled_by_default? (line 52) | def ui_enabled_by_default?
method env_enabled? (line 56) | def env_enabled?(key)
method migrations_storage_from_env (line 60) | def migrations_storage_from_env
method parse_excluded_databases_env (line 64) | def parse_excluded_databases_env
method apply_defaults (line 74) | def apply_defaults(settings)
FILE: lib/actual_db_schema/console_migrations.rb
type ActualDbSchema (line 3) | module ActualDbSchema
type ConsoleMigrations (line 5) | module ConsoleMigrations
function migration_instance (line 43) | def migration_instance
FILE: lib/actual_db_schema/engine.rb
type ActualDbSchema (line 3) | module ActualDbSchema
class Engine (line 5) | class Engine < ::Rails::Engine
method apply_schema_dump_exclusions (line 22) | def self.apply_schema_dump_exclusions
method ignore_schema_dump_table (line 35) | def ignore_schema_dump_table(table_name)
method schema_dump_flags_supported? (line 42) | def schema_dump_flags_supported?
method schema_dump_connection_available? (line 49) | def schema_dump_connection_available?
method apply_structure_dump_flags (line 59) | def apply_structure_dump_flags(table_name)
method database_name (line 75) | def database_name
FILE: lib/actual_db_schema/failed_migration.rb
type ActualDbSchema (line 3) | module ActualDbSchema
function filename (line 5) | def filename
function short_filename (line 9) | def short_filename
FILE: lib/actual_db_schema/git.rb
type ActualDbSchema (line 3) | module ActualDbSchema
class Git (line 5) | class Git
method current_branch (line 6) | def self.current_branch
FILE: lib/actual_db_schema/git_hooks.rb
type ActualDbSchema (line 5) | module ActualDbSchema
class GitHooks (line 7) | class GitHooks
method initialize (line 61) | def initialize(strategy: :rollback)
method install_post_checkout_hook (line 65) | def install_post_checkout_hook
method hook_code (line 77) | def hook_code
method hooks_dir (line 81) | def hooks_dir
method hook_path (line 85) | def hook_path
method hooks_directory_present? (line 89) | def hooks_directory_present?
method handle_existing_hook (line 95) | def handle_existing_hook
method create_new_hook (line 102) | def create_new_hook
method markers_exist? (line 113) | def markers_exist?
method update_hook (line 118) | def update_hook
method replace_marker_contents (line 131) | def replace_marker_contents(contents)
method safe_install? (line 138) | def safe_install?
method install_hook (line 146) | def install_hook
method show_manual_install_instructions (line 158) | def show_manual_install_instructions
method write_hook_file (line 178) | def write_hook_file(contents)
method print_success (line 183) | def print_success
FILE: lib/actual_db_schema/instrumentation.rb
type ActualDbSchema (line 3) | module ActualDbSchema
type Instrumentation (line 4) | module Instrumentation
FILE: lib/actual_db_schema/migration.rb
type ActualDbSchema (line 3) | module ActualDbSchema
class Migration (line 5) | class Migration
method all_phantom (line 11) | def all_phantom
method all (line 26) | def all
method find (line 41) | def find(version, database)
method rollback (line 51) | def rollback(version, database)
method rollback_all (line 62) | def rollback_all
method migrate (line 66) | def migrate(version, database)
method broken_versions (line 77) | def broken_versions
method delete (line 96) | def delete(version, database)
method delete_all (line 108) | def delete_all
method build_migration_struct (line 116) | def build_migration_struct(status, migration)
method sort_migrations_desc (line 129) | def sort_migrations_desc(migrations)
method phantom? (line 133) | def phantom?(migration)
method should_include? (line 137) | def should_include?(status, migration)
method find_migration_in_context (line 141) | def find_migration_in_context(context, version)
method branch_for (line 149) | def branch_for(version)
method metadata (line 153) | def metadata
method validate_broken_migration (line 158) | def validate_broken_migration(version, database)
FILE: lib/actual_db_schema/migration_context.rb
type ActualDbSchema (line 3) | module ActualDbSchema
class MigrationContext (line 5) | class MigrationContext
method each (line 8) | def each
method establish_connection (line 20) | def establish_connection(db_config)
method current_config (line 25) | def current_config
method configs (line 33) | def configs
method filter_configs (line 44) | def filter_configs(all_configs)
method context (line 59) | def context
FILE: lib/actual_db_schema/migration_parser.rb
type ActualDbSchema (line 6) | module ActualDbSchema
type MigrationParser (line 8) | module MigrationParser
function parse_all_migrations (line 23) | def parse_all_migrations(dirs)
function parse_file (line 43) | def parse_file(file_path)
function find_migration_changes (line 47) | def find_migration_changes(node)
function process_block_node (line 62) | def process_block_node(node)
function process_send_node (line 75) | def process_send_node(node)
function parse_add_column (line 86) | def parse_add_column(args)
function parse_change_column (line 98) | def parse_change_column(args)
function parse_remove_column (line 110) | def parse_remove_column(args)
function parse_rename_column (line 121) | def parse_rename_column(args)
function parse_add_index (line 132) | def parse_add_index(args)
function parse_remove_index (line 143) | def parse_remove_index(args)
function parse_rename_index (line 153) | def parse_rename_index(args)
function parse_create_table (line 164) | def parse_create_table(args)
function parse_drop_table (line 174) | def parse_drop_table(args)
function parse_create_table_with_block (line 184) | def parse_create_table_with_block(send_node, block_node)
function parse_create_table_columns (line 195) | def parse_create_table_columns(body_node)
function parse_column_node (line 202) | def parse_column_node(node)
function parse_timestamps (line 215) | def parse_timestamps
function sym_value (line 222) | def sym_value(node)
function array_or_single_value (line 228) | def array_or_single_value(node)
function parse_hash (line 238) | def parse_hash(node)
function node_value (line 249) | def node_value(node)
FILE: lib/actual_db_schema/multi_tenant.rb
type ActualDbSchema (line 3) | module ActualDbSchema
type MultiTenant (line 5) | module MultiTenant
function with_schema (line 9) | def with_schema(schema_name)
function adapter_name (line 18) | def adapter_name
function switch_schema (line 22) | def switch_schema(schema_name)
function switch_postgresql_schema (line 35) | def switch_postgresql_schema(schema_name)
function switch_mysql_schema (line 41) | def switch_mysql_schema(schema_name)
function restore_context (line 47) | def restore_context(context)
FILE: lib/actual_db_schema/output_formatter.rb
type ActualDbSchema (line 3) | module ActualDbSchema
type OutputFormatter (line 5) | module OutputFormatter
function colorize (line 13) | def colorize(text, color)
FILE: lib/actual_db_schema/patches/migration_context.rb
type ActualDbSchema (line 3) | module ActualDbSchema
type Patches (line 4) | module Patches
type MigrationContext (line 6) | module MigrationContext
function rollback_branches (line 9) | def rollback_branches(manual_mode: false)
function phantom_migrations (line 23) | def phantom_migrations
function rollback_branches_for_schema (line 35) | def rollback_branches_for_schema(manual_mode: false, schema_name: ...
function rollback_multi_tenant (line 51) | def rollback_multi_tenant(schemas, manual_mode: false)
function down_migrator_for (line 64) | def down_migrator_for(migration)
function migration_files (line 74) | def migration_files
function status_up? (line 87) | def status_up?(migration)
function user_wants_rollback? (line 93) | def user_wants_rollback?
function show_info_for (line 99) | def show_info_for(migration, schema_name = nil)
function migrate (line 109) | def migrate(migration, rolled_back_migrations, schema_name = nil, ...
function notify_rollback_migration (line 127) | def notify_rollback_migration(migration:, schema_name:, branch:, m...
function extract_class_name (line 139) | def extract_class_name(filename)
function branch_for (line 144) | def branch_for(version)
function metadata (line 148) | def metadata
function handle_rollback_error (line 152) | def handle_rollback_error(migration, exception, schema_name = nil)
function cleaned_exception_message (line 168) | def cleaned_exception_message(message)
function delete_migrations (line 177) | def delete_migrations(migrations, schema_count)
function multi_tenant_schemas (line 188) | def multi_tenant_schemas
FILE: lib/actual_db_schema/patches/migration_proxy.rb
type ActualDbSchema (line 3) | module ActualDbSchema
type Patches (line 4) | module Patches
type MigrationProxy (line 6) | module MigrationProxy
function migrate (line 7) | def migrate(direction)
FILE: lib/actual_db_schema/patches/migrator.rb
type ActualDbSchema (line 3) | module ActualDbSchema
type Patches (line 4) | module Patches
type Migrator (line 6) | module Migrator
function runnable (line 7) | def runnable
FILE: lib/actual_db_schema/railtie.rb
type ActualDbSchema (line 3) | module ActualDbSchema
class Railtie (line 5) | class Railtie < ::Rails::Railtie
FILE: lib/actual_db_schema/rollback_stats_repository.rb
type ActualDbSchema (line 3) | module ActualDbSchema
class RollbackStatsRepository (line 5) | class RollbackStatsRepository
method record (line 9) | def record(payload)
method stats (line 24) | def stats
method total_rollbacks (line 35) | def total_rollbacks
method reset! (line 43) | def reset!
method ensure_table! (line 51) | def ensure_table!
method table_exists? (line 65) | def table_exists?
method aggregate_by (line 69) | def aggregate_by(column)
method empty_stats (line 80) | def empty_stats
method connection (line 89) | def connection
method quoted_table (line 93) | def quoted_table
method quoted_column (line 97) | def quoted_column(name)
FILE: lib/actual_db_schema/schema_diff.rb
type ActualDbSchema (line 5) | module ActualDbSchema
class SchemaDiff (line 8) | class SchemaDiff
method initialize (line 30) | def initialize(schema_path, migrations_path)
method render (line 35) | def render
method old_schema_content (line 47) | def old_schema_content
method new_schema_content (line 54) | def new_schema_content
method parsed_old_schema (line 58) | def parsed_old_schema
method parsed_new_schema (line 62) | def parsed_new_schema
method parser_class (line 66) | def parser_class
method structure_sql? (line 70) | def structure_sql?
method migration_changes (line 74) | def migration_changes
method migrated_folders (line 81) | def migrated_folders
method find_migrated_folders (line 92) | def find_migrated_folders
method generate_diff (line 103) | def generate_diff(old_content, new_content)
method process_diff_output (line 116) | def process_diff_output(diff_str)
method handle_diff_line (line 135) | def handle_diff_line(line, current_table)
method detect_action_and_name (line 147) | def detect_action_and_name(line_content, sign, current_table)
method guess_action (line 169) | def guess_action(sign, table, col_name)
method find_table_in_new_schema (line 180) | def find_table_in_new_schema(new_line_number)
method find_migrations (line 191) | def find_migrations(action, table_name, col_or_index_name)
method index_action? (line 205) | def index_action?(action)
method migration_matches? (line 209) | def migration_matches?(chg, action, col_or_index_name)
method rename_column_matches? (line 222) | def rename_column_matches?(chg, action, col)
method rename_index_matches? (line 227) | def rename_index_matches?(chg, action, name)
method index_matches? (line 232) | def index_matches?(chg, action, col_or_index_name)
method column_matches? (line 238) | def column_matches?(chg, action, col_name)
method extract_migration_index_name (line 242) | def extract_migration_index_name(chg, table_name)
method annotate_line (line 251) | def annotate_line(line, migration_file_paths)
method normalize_table_name (line 255) | def normalize_table_name(table_name)
FILE: lib/actual_db_schema/schema_diff_html.rb
type ActualDbSchema (line 3) | module ActualDbSchema
class SchemaDiffHtml (line 6) | class SchemaDiffHtml < SchemaDiff
method render_html (line 7) | def render_html(table_filter)
method generate_diff_html (line 18) | def generate_diff_html
method generate_full_diff (line 25) | def generate_full_diff(old_content, new_content)
method process_diff_output_for_html (line 38) | def process_diff_output_for_html(diff_str)
method skip_line? (line 56) | def skip_line?(line)
method process_table (line 61) | def process_table(line, current_table, table_start, table_end, block...
method handle_diff_line_html (line 81) | def handle_diff_line_html(line, current_table)
method annotate_line (line 91) | def annotate_line(line, migration_file_paths, color)
method colorize_html (line 96) | def colorize_html(text, color)
method link_to_migration (line 109) | def link_to_migration(migration_file_path)
method migrations (line 117) | def migrations
method extract_table_section (line 121) | def extract_table_section(full_diff_html, table_name)
FILE: lib/actual_db_schema/schema_parser.rb
type ActualDbSchema (line 6) | module ActualDbSchema
type SchemaParser (line 8) | module SchemaParser
function parse_string (line 11) | def parse_string(schema_content)
class SchemaCollector (line 20) | class SchemaCollector < Parser::AST::Processor
method initialize (line 23) | def initialize
method on_block (line 28) | def on_block(node)
method on_send (line 40) | def on_send(node)
method create_table_call? (line 52) | def create_table_call?(node)
method extract_table_name (line 59) | def extract_table_name(send_node)
method extract_columns (line 69) | def extract_columns(body_node)
method process_column_node (line 82) | def process_column_node(node)
method extract_column_name (line 98) | def extract_column_name(node)
method extract_column_options (line 107) | def extract_column_options(args)
method parse_hash (line 117) | def parse_hash(node)
method extract_key (line 130) | def extract_key(node)
method extract_literal (line 139) | def extract_literal(node)
FILE: lib/actual_db_schema/store.rb
type ActualDbSchema (line 3) | module ActualDbSchema
class Store (line 5) | class Store
method write (line 10) | def write(filename)
method read (line 15) | def read
method migration_files (line 19) | def migration_files
method delete (line 23) | def delete(filename)
method stored_migration? (line 28) | def stored_migration?(filename)
method source_for (line 32) | def source_for(version)
method materialize_all (line 41) | def materialize_all
method reset_adapter (line 45) | def reset_adapter
method adapter (line 52) | def adapter
method reset_source_cache (line 59) | def reset_source_cache
method db_versions (line 64) | def db_versions
method file_versions (line 78) | def file_versions
class FileAdapter (line 85) | class FileAdapter
method write (line 86) | def write(filename)
method read (line 93) | def read
method migration_files (line 99) | def migration_files
method delete (line 103) | def delete(filename)
method stored_migration? (line 107) | def stored_migration?(filename)
method materialize_all (line 111) | def materialize_all
method record_metadata (line 117) | def record_metadata(filename)
method folder (line 128) | def folder
method store_file (line 132) | def store_file
class DbAdapter (line 138) | class DbAdapter
method write (line 142) | def write(filename)
method read (line 154) | def read
method migration_files (line 167) | def migration_files
method delete (line 172) | def delete(filename)
method stored_migration? (line 185) | def stored_migration?(filename)
method materialize_all (line 189) | def materialize_all
method upsert_record (line 205) | def upsert_record(version, basename, content, branch, migrated_at)
method record_attributes (line 210) | def record_attributes(version, basename, content, branch, migrated...
method update_record (line 220) | def update_record(attributes)
method insert_record (line 232) | def insert_record(attributes)
method record_exists? (line 244) | def record_exists?(version)
method ensure_table! (line 253) | def ensure_table!
method table_exists? (line 267) | def table_exists?
method connection (line 271) | def connection
method record_columns (line 275) | def record_columns
method quoted_table (line 279) | def quoted_table
method quoted_column (line 283) | def quoted_column(name)
method folder (line 287) | def folder
method write_cache_file (line 291) | def write_cache_file(filename, content)
method extract_version (line 299) | def extract_version(filename)
FILE: lib/actual_db_schema/structure_sql_parser.rb
type ActualDbSchema (line 3) | module ActualDbSchema
type StructureSqlParser (line 5) | module StructureSqlParser
function parse_string (line 8) | def parse_string(sql_content)
function parse_columns (line 17) | def parse_columns(columns_section)
function normalize_table_name (line 35) | def normalize_table_name(table_name)
FILE: lib/actual_db_schema/version.rb
type ActualDbSchema (line 3) | module ActualDbSchema
FILE: lib/tasks/test.rake
function wait_for_postgres (line 42) | def wait_for_postgres
function wait_for_mysql (line 56) | def wait_for_mysql
FILE: test/controllers/actual_db_schema/broken_versions_controller_db_storage_test.rb
type ActualDbSchema (line 6) | module ActualDbSchema
class BrokenVersionsControllerDbStorageTest (line 7) | class BrokenVersionsControllerDbStorageTest < ActionController::TestCase
method setup (line 10) | def setup
method teardown (line 20) | def teardown
method routes_setup (line 25) | def routes_setup
method active_record_setup (line 38) | def active_record_setup
method setup_utils (line 43) | def setup_utils
method configure_storage (line 47) | def configure_storage
method configure_app (line 51) | def configure_app
method configure_views (line 56) | def configure_views
method prepare_database (line 60) | def prepare_database
method delete_migrations_files (line 67) | def delete_migrations_files
method delete_primary_migrations (line 72) | def delete_primary_migrations
method delete_secondary_migrations (line 83) | def delete_secondary_migrations
FILE: test/controllers/actual_db_schema/broken_versions_controller_test.rb
type ActualDbSchema (line 6) | module ActualDbSchema
class BrokenVersionsControllerTest (line 7) | class BrokenVersionsControllerTest < ActionController::TestCase
method setup (line 8) | def setup
method routes_setup (line 20) | def routes_setup
method active_record_setup (line 33) | def active_record_setup
method delete_migrations_files (line 38) | def delete_migrations_files
FILE: test/controllers/actual_db_schema/migrations_controller_db_storage_test.rb
type ActualDbSchema (line 6) | module ActualDbSchema
class MigrationsControllerDbStorageTest (line 7) | class MigrationsControllerDbStorageTest < ActionController::TestCase
method setup (line 10) | def setup
method teardown (line 20) | def teardown
method routes_setup (line 25) | def routes_setup
method active_record_setup (line 39) | def active_record_setup
method setup_utils (line 44) | def setup_utils
method configure_storage (line 48) | def configure_storage
method configure_app (line 52) | def configure_app
method configure_views (line 57) | def configure_views
method prepare_database (line 61) | def prepare_database
FILE: test/controllers/actual_db_schema/migrations_controller_test.rb
type ActualDbSchema (line 6) | module ActualDbSchema
class MigrationsControllerTest (line 7) | class MigrationsControllerTest < ActionController::TestCase
method setup (line 8) | def setup
method routes_setup (line 20) | def routes_setup
method active_record_setup (line 34) | def active_record_setup
FILE: test/controllers/actual_db_schema/phantom_migrations_controller_db_storage_test.rb
type ActualDbSchema (line 6) | module ActualDbSchema
class PhantomMigrationsControllerDbStorageTest (line 7) | class PhantomMigrationsControllerDbStorageTest < ActionController::Tes...
method setup (line 10) | def setup
method teardown (line 20) | def teardown
method routes_setup (line 25) | def routes_setup
method active_record_setup (line 39) | def active_record_setup
method setup_utils (line 44) | def setup_utils
method configure_storage (line 48) | def configure_storage
method configure_app (line 52) | def configure_app
method configure_views (line 57) | def configure_views
method prepare_database (line 61) | def prepare_database
FILE: test/controllers/actual_db_schema/phantom_migrations_controller_test.rb
type ActualDbSchema (line 6) | module ActualDbSchema
class PhantomMigrationsControllerTest (line 7) | class PhantomMigrationsControllerTest < ActionController::TestCase
method setup (line 8) | def setup
method routes_setup (line 20) | def routes_setup
method active_record_setup (line 34) | def active_record_setup
FILE: test/controllers/actual_db_schema/schema_controller_db_storage_test.rb
type ActualDbSchema (line 6) | module ActualDbSchema
class SchemaControllerDbStorageTest (line 7) | class SchemaControllerDbStorageTest < ActionController::TestCase
method setup (line 10) | def setup
method teardown (line 21) | def teardown
method routes_setup (line 41) | def routes_setup
method active_record_setup (line 50) | def active_record_setup
method setup_utils (line 55) | def setup_utils
method configure_storage (line 59) | def configure_storage
method configure_app (line 63) | def configure_app
method configure_views (line 68) | def configure_views
method prepare_database (line 72) | def prepare_database
method stub_schema_diff (line 79) | def stub_schema_diff
method define_migrations (line 86) | def define_migrations
FILE: test/controllers/actual_db_schema/schema_controller_test.rb
type ActualDbSchema (line 6) | module ActualDbSchema
class SchemaControllerTest (line 7) | class SchemaControllerTest < ActionController::TestCase
method setup (line 8) | def setup
method teardown (line 20) | def teardown
method routes_setup (line 38) | def routes_setup
method active_record_setup (line 47) | def active_record_setup
method define_migrations (line 52) | def define_migrations
method run_migration (line 76) | def run_migration(file_name, content)
method dump_schema (line 82) | def dump_schema
method define_schema_diff_html_methods_for_schema_rb (line 94) | def define_schema_diff_html_methods_for_schema_rb
method define_schema_diff_html_methods_for_structure_sql (line 103) | def define_schema_diff_html_methods_for_structure_sql
method add_surname_to_users_migration (line 112) | def add_surname_to_users_migration
FILE: test/rake_task_db_storage_full_test.rb
function collect_rollback_events (line 19) | def collect_rollback_events
FILE: test/rake_task_delete_broken_versions_db_storage_test.rb
function delete_migration_files (line 23) | def delete_migration_files
function remove_primary_migration_files (line 30) | def remove_primary_migration_files
function remove_secondary_migration_files (line 35) | def remove_secondary_migration_files
function delete_primary_storage_entries (line 40) | def delete_primary_storage_entries
function delete_secondary_storage_entries (line 45) | def delete_secondary_storage_entries
FILE: test/rake_task_delete_broken_versions_test.rb
function delete_migration_files (line 21) | def delete_migration_files
FILE: test/rake_task_schema_diff_db_storage_test.rb
function invoke_rake_task (line 85) | def invoke_rake_task
function migration_path (line 92) | def migration_path(file_name)
FILE: test/rake_task_schema_diff_test.rb
function migration_path (line 60) | def migration_path(file_name)
function invoke_rake_task (line 64) | def invoke_rake_task(schema_path)
function run_migration (line 69) | def run_migration(file_name, content)
function dump_schema (line 75) | def dump_schema
function add_surname_to_users_migration (line 369) | def add_surname_to_users_migration
function remove_middle_name_from_users_migration (line 379) | def remove_middle_name_from_users_migration
function change_price_precision_in_products_migration (line 389) | def change_price_precision_in_products_migration
function rename_name_to_full_name_in_users_migration (line 399) | def rename_name_to_full_name_in_users_migration
function add_index_on_users_middle_name_migration (line 409) | def add_index_on_users_middle_name_migration
function remove_index_on_users_name_migration (line 419) | def remove_index_on_users_name_migration
function rename_index_on_users_name_migration (line 429) | def rename_index_on_users_name_migration
function create_categories_migration (line 439) | def create_categories_migration
function drop_categories_migration (line 452) | def drop_categories_migration
function drop_products_table_migration (line 462) | def drop_products_table_migration
function phantom_migration (line 472) | def phantom_migration
FILE: test/rake_task_test.rb
function collect_rollback_events (line 17) | def collect_rollback_events
FILE: test/support/test_utils.rb
class TestUtils (line 3) | class TestUtils
method initialize (line 16) | def initialize(migrations_path: "db/migrate", migrated_path: "tmp/migr...
method app_file (line 27) | def app_file(path)
method remove_app_dir (line 31) | def remove_app_dir(name)
method run_migrations (line 35) | def run_migrations
method applied_migrations (line 44) | def applied_migrations(db_config = nil)
method simulate_input (line 55) | def simulate_input(input)
method delete_migrations_files (line 60) | def delete_migrations_files(prefix_name = nil)
method delete_migrations_files_for (line 65) | def delete_migrations_files_for(path)
method define_migration_file (line 71) | def define_migration_file(filename, content, prefix: nil)
method define_migrations (line 86) | def define_migrations(prefix_name = nil)
method reset_database_yml (line 108) | def reset_database_yml(db_config)
method cleanup_config_files (line 118) | def cleanup_config_files(db_config)
method prepare_phantom_migrations (line 127) | def prepare_phantom_migrations(db_config = nil)
method cleanup (line 138) | def cleanup(db_config = nil)
method clear_db_storage_table (line 151) | def clear_db_storage_table(db_config = nil)
method drop_db_storage_table (line 162) | def drop_db_storage_table
method migrated_files (line 169) | def migrated_files(db_config = nil)
method branch_for (line 180) | def branch_for(version)
method define_acronym (line 184) | def define_acronym(acronym)
method reset_acronyms (line 190) | def reset_acronyms
method primary_database (line 200) | def primary_database
method secondary_database (line 204) | def secondary_database
method run_migration_tasks (line 210) | def run_migration_tasks
method cleanup_call (line 220) | def cleanup_call(prefix_name = nil)
method create_schema_migration_table (line 230) | def create_schema_migration_table
method schema_migration_class (line 234) | def schema_migration_class
method migrated_files_call (line 247) | def migrated_files_call(prefix_name = nil)
method clear_schema_call (line 253) | def clear_schema_call
method applied_migrations_call (line 257) | def applied_migrations_call
method run_sql (line 263) | def run_sql(sql)
method metadata (line 267) | def metadata
FILE: test/test_actual_db_schema.rb
class TestActualDbSchema (line 5) | class TestActualDbSchema < Minitest::Test
method test_that_it_has_a_version_number (line 6) | def test_that_it_has_a_version_number
FILE: test/test_actual_db_schema_db_storage_test.rb
class TestActualDbSchemaDbStorage (line 5) | class TestActualDbSchemaDbStorage < Minitest::Test
method setup (line 6) | def setup
method teardown (line 10) | def teardown
method test_that_it_has_a_version_number (line 14) | def test_that_it_has_a_version_number
FILE: test/test_database_filtering.rb
function config_name (line 14) | def config_name(db_config)
FILE: test/test_helper.rb
class FakeApplication (line 19) | class FakeApplication < Rails::Application
method initialize (line 20) | def initialize
class TestingState (line 28) | class TestingState
method reset (line 33) | def self.reset
method db_config (line 40) | def self.db_config
method sqlite3_config (line 55) | def self.sqlite3_config
method postgresql_config (line 70) | def self.postgresql_config
method mysql2_config (line 93) | def self.mysql2_config
type Minitest (line 121) | module Minitest
class Test (line 122) | class Test
method before_setup (line 123) | def before_setup
method cleanup_migrated_cache (line 139) | def cleanup_migrated_cache
method clear_db_storage_tables (line 144) | def clear_db_storage_tables
method db_storage_configs (line 153) | def db_storage_configs
method drop_db_storage_table (line 160) | def drop_db_storage_table(conn)
method drop_db_storage_table_in_schemas (line 169) | def drop_db_storage_table_in_schemas(conn, table_name)
type Kernel (line 182) | module Kernel
function puts (line 185) | def puts(*args)
FILE: test/test_migration_context.rb
function current_database (line 39) | def current_database
Condensed preview — 101 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (354K chars).
[
{
"path": ".github/workflows/main.yml",
"chars": 1232,
"preview": "name: Ruby\n\non:\n push:\n branches:\n - main\n\n pull_request:\n\njobs:\n build:\n runs-on: ubuntu-latest\n name:"
},
{
"path": ".gitignore",
"chars": 266,
"preview": "/.bundle/\n/.yardoc\n/_yardoc/\n/coverage/\n/doc/\n/pkg/\n/spec/reports/\n/tmp/\n/test/dummy_app/tmp/\n/test/dummy_app/custom/\n/t"
},
{
"path": ".rubocop.yml",
"chars": 656,
"preview": "AllCops:\n TargetRubyVersion: 2.7\n Exclude:\n - gemfiles/*\n - test/dummy_app/**/*\n - vendor/**/*\n\nStyle/StringL"
},
{
"path": "Appraisals",
"chars": 335,
"preview": "# frozen_string_literal: true\n\n%w[6.0 6.1 7.0 7.1].each do |version|\n appraise \"rails.#{version}\" do\n gem \"activerec"
},
{
"path": "CHANGELOG.md",
"chars": 3971,
"preview": "## [0.9.1] - 2026-02-25\n\n- Support schema diffs for `structure.sql`\n- Add an option to exclude specific databases from t"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 5214,
"preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participa"
},
{
"path": "Gemfile",
"chars": 257,
"preview": "# frozen_string_literal: true\n\nsource \"https://rubygems.org\"\n\n# Specify your gem's dependencies in actual_db_schema.gems"
},
{
"path": "LICENSE.txt",
"chars": 1082,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2022 Andrei Kaleshka\n\nPermission is hereby granted, free of charge, to any person o"
},
{
"path": "README.md",
"chars": 20447,
"preview": "[](https://badge.fury.io/rb/actual_db_schema)\n\n# ActualDbSc"
},
{
"path": "Rakefile",
"chars": 330,
"preview": "# frozen_string_literal: true\n\nrequire \"bundler/gem_tasks\"\nrequire \"rake/testtask\"\n\nload \"lib/tasks/test.rake\"\n\nRake::Te"
},
{
"path": "actual_db_schema.gemspec",
"chars": 2688,
"preview": "# frozen_string_literal: true\n\nrequire_relative \"lib/actual_db_schema/version\"\n\nGem::Specification.new do |spec|\n spec."
},
{
"path": "app/controllers/actual_db_schema/broken_versions_controller.rb",
"chars": 1089,
"preview": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n # Controller for managing broken migration versions.\n class Brok"
},
{
"path": "app/controllers/actual_db_schema/migrations_controller.rb",
"chars": 1801,
"preview": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n # Controller to display the list of migrations for each database "
},
{
"path": "app/controllers/actual_db_schema/phantom_migrations_controller.rb",
"chars": 1429,
"preview": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n # Controller to display the list of phantom migrations for each d"
},
{
"path": "app/controllers/actual_db_schema/schema_controller.rb",
"chars": 584,
"preview": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n # Controller to display the database schema diff.\n class SchemaC"
},
{
"path": "app/views/actual_db_schema/broken_versions/index.html.erb",
"chars": 2556,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <title>Broken Versions</title>\n <%= render partial: 'actual_db_schema/shared/js' "
},
{
"path": "app/views/actual_db_schema/migrations/index.html.erb",
"chars": 3447,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <title>Migrations</title>\n <%= render partial: 'actual_db_schema/shared/js' %>\n "
},
{
"path": "app/views/actual_db_schema/migrations/show.html.erb",
"chars": 2114,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <title>Migration Details</title>\n <%= render partial: 'actual_db_schema/shared/js"
},
{
"path": "app/views/actual_db_schema/phantom_migrations/index.html.erb",
"chars": 2390,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <title>Phantom Migrations</title>\n <%= render partial: 'actual_db_schema/shared/j"
},
{
"path": "app/views/actual_db_schema/phantom_migrations/show.html.erb",
"chars": 1783,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <title>Phantom Migration Details</title>\n <%= render partial: 'actual_db_schema/s"
},
{
"path": "app/views/actual_db_schema/schema/index.html.erb",
"chars": 964,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <title>Database Schema</title>\n <%= render partial: 'actual_db_schema/shared/js' "
},
{
"path": "app/views/actual_db_schema/shared/_js.html",
"chars": 1345,
"preview": "<script>\n document.addEventListener('DOMContentLoaded', function() {\n const migrationActions = document.querySelecto"
},
{
"path": "app/views/actual_db_schema/shared/_style.html",
"chars": 2806,
"preview": "<style>\n body {\n margin: 8px;\n background-color: #fff;\n color: #333;\n }\n\n body, p, td {\n font-family: hel"
},
{
"path": "bin/console",
"chars": 381,
"preview": "#!/usr/bin/env ruby\n# frozen_string_literal: true\n\nrequire \"bundler/setup\"\nrequire \"actual_db_schema\"\n\n# You can add fix"
},
{
"path": "bin/setup",
"chars": 131,
"preview": "#!/usr/bin/env bash\nset -euo pipefail\nIFS=$'\\n\\t'\nset -vx\n\nbundle install\n\n# Do any other automated setup that you need "
},
{
"path": "config/routes.rb",
"chars": 542,
"preview": "# frozen_string_literal: true\n\nActualDbSchema::Engine.routes.draw do\n resources :migrations, only: %i[index show] do\n "
},
{
"path": "docker/mysql-init/create_secondary_db.sql",
"chars": 49,
"preview": "CREATE DATABASE actual_db_schema_test_secondary;\n"
},
{
"path": "docker/postgres-init/create_secondary_db.sql",
"chars": 49,
"preview": "CREATE DATABASE actual_db_schema_test_secondary;\n"
},
{
"path": "docker-compose.yml",
"chars": 512,
"preview": "version: '3.8'\n\nservices:\n postgres:\n image: postgres:14\n environment:\n POSTGRES_USER: postgres\n POSTGR"
},
{
"path": "gemfiles/rails.6.0.gemfile",
"chars": 317,
"preview": "# frozen_string_literal: true\n\n# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"activerecord\""
},
{
"path": "gemfiles/rails.6.1.gemfile",
"chars": 317,
"preview": "# frozen_string_literal: true\n\n# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"activerecord\""
},
{
"path": "gemfiles/rails.7.0.gemfile",
"chars": 317,
"preview": "# frozen_string_literal: true\n\n# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"activerecord\""
},
{
"path": "gemfiles/rails.7.1.gemfile",
"chars": 317,
"preview": "# frozen_string_literal: true\n\n# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"activerecord\""
},
{
"path": "gemfiles/rails.edge.gemfile",
"chars": 344,
"preview": "# frozen_string_literal: true\n\n# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"activerecord\""
},
{
"path": "lib/actual_db_schema/commands/base.rb",
"chars": 532,
"preview": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n module Commands\n # Base class for all commands\n class Base\n"
},
{
"path": "lib/actual_db_schema/commands/list.rb",
"chars": 2045,
"preview": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n module Commands\n # Shows the list of phantom migrations\n cl"
},
{
"path": "lib/actual_db_schema/commands/rollback.rb",
"chars": 2532,
"preview": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n module Commands\n # Rolls back all phantom migrations\n class"
},
{
"path": "lib/actual_db_schema/configuration.rb",
"chars": 2196,
"preview": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n # Manages the configuration settings for the gem.\n class Configu"
},
{
"path": "lib/actual_db_schema/console_migrations.rb",
"chars": 1135,
"preview": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n # Provides methods for executing schema modification commands dir"
},
{
"path": "lib/actual_db_schema/engine.rb",
"chars": 2918,
"preview": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n # It isolates the namespace to avoid conflicts with the main appl"
},
{
"path": "lib/actual_db_schema/failed_migration.rb",
"chars": 302,
"preview": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n FailedMigration = Struct.new(:migration, :exception, :branch, :sc"
},
{
"path": "lib/actual_db_schema/git.rb",
"chars": 275,
"preview": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n # Git helper\n class Git\n def self.current_branch\n branch"
},
{
"path": "lib/actual_db_schema/git_hooks.rb",
"chars": 5358,
"preview": "# frozen_string_literal: true\n\nrequire \"fileutils\"\n\nmodule ActualDbSchema\n # Handles the installation of a git post-che"
},
{
"path": "lib/actual_db_schema/instrumentation.rb",
"chars": 137,
"preview": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n module Instrumentation\n ROLLBACK_EVENT = \"rollback.actual_db_s"
},
{
"path": "lib/actual_db_schema/migration.rb",
"chars": 5124,
"preview": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n # The Migration class is responsible for managing and retrieving "
},
{
"path": "lib/actual_db_schema/migration_context.rb",
"chars": 2355,
"preview": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n # The class manages connections to each database and provides the"
},
{
"path": "lib/actual_db_schema/migration_parser.rb",
"chars": 6480,
"preview": "# frozen_string_literal: true\n\nrequire \"ast\"\nrequire \"prism\"\n\nmodule ActualDbSchema\n # Parses migration files in a Rail"
},
{
"path": "lib/actual_db_schema/multi_tenant.rb",
"chars": 1928,
"preview": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n # Handles multi-tenancy support by switching schemas for supporte"
},
{
"path": "lib/actual_db_schema/output_formatter.rb",
"chars": 375,
"preview": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n # Provides functionality for formatting terminal output with colo"
},
{
"path": "lib/actual_db_schema/patches/migration_context.rb",
"chars": 7148,
"preview": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n module Patches\n # Add new command to roll back the phantom mig"
},
{
"path": "lib/actual_db_schema/patches/migration_proxy.rb",
"chars": 331,
"preview": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n module Patches\n # Records the migration file into the tmp fold"
},
{
"path": "lib/actual_db_schema/patches/migrator.rb",
"chars": 332,
"preview": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n module Patches\n # Run only one migration that's being rolled b"
},
{
"path": "lib/actual_db_schema/railtie.rb",
"chars": 498,
"preview": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n # Integrates the ConsoleMigrations module into the Rails console."
},
{
"path": "lib/actual_db_schema/rollback_stats_repository.rb",
"chars": 2839,
"preview": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n # Persists rollback events in DB.\n class RollbackStatsRepository"
},
{
"path": "lib/actual_db_schema/schema_diff.rb",
"chars": 8459,
"preview": "# frozen_string_literal: true\n\nrequire \"tempfile\"\n\nmodule ActualDbSchema\n # Generates a diff of schema changes between "
},
{
"path": "lib/actual_db_schema/schema_diff_html.rb",
"chars": 4243,
"preview": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n # Generates an HTML representation of the schema diff,\n # annota"
},
{
"path": "lib/actual_db_schema/schema_parser.rb",
"chars": 3873,
"preview": "# frozen_string_literal: true\n\nrequire \"parser/ast/processor\"\nrequire \"prism\"\n\nmodule ActualDbSchema\n # Parses the cont"
},
{
"path": "lib/actual_db_schema/store.rb",
"chars": 7627,
"preview": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n # Stores migration sources and metadata.\n class Store\n includ"
},
{
"path": "lib/actual_db_schema/structure_sql_parser.rb",
"chars": 1162,
"preview": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n # Parses the content of a `structure.sql` file into a structured "
},
{
"path": "lib/actual_db_schema/version.rb",
"chars": 77,
"preview": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n VERSION = \"0.9.1\"\nend\n"
},
{
"path": "lib/actual_db_schema.rb",
"chars": 2757,
"preview": "# frozen_string_literal: true\n\nrequire \"actual_db_schema/engine\"\nrequire \"active_record/migration\"\nrequire \"csv\"\nrequire"
},
{
"path": "lib/generators/actual_db_schema/templates/actual_db_schema.rb",
"chars": 1872,
"preview": "# frozen_string_literal: true\n\n# ActualDbSchema initializer\n# Adjust the configuration as needed.\n\nif defined?(ActualDbS"
},
{
"path": "lib/tasks/actual_db_schema.rake",
"chars": 3679,
"preview": "# frozen_string_literal: true\n\nnamespace :actual_db_schema do # rubocop:disable Metrics/BlockLength\n desc \"Install Actu"
},
{
"path": "lib/tasks/db.rake",
"chars": 989,
"preview": "# frozen_string_literal: true\n\nnamespace :db do\n desc \"Rollback migrations that were run inside not a merged branch.\"\n "
},
{
"path": "lib/tasks/test.rake",
"chars": 1483,
"preview": "# frozen_string_literal: true\n\nnamespace :test do # rubocop:disable Metrics/BlockLength\n desc \"Run tests with SQLite3\"\n"
},
{
"path": "sig/actual_db_schema.rbs",
"chars": 113,
"preview": "module ActualDbSchema\n VERSION: String\n # See the writing guide of rbs: https://github.com/ruby/rbs#guides\nend\n"
},
{
"path": "test/controllers/actual_db_schema/broken_versions_controller_db_storage_test.rb",
"chars": 6385,
"preview": "# frozen_string_literal: true\n\nrequire_relative \"../../test_helper\"\nrequire_relative \"../../../app/controllers/actual_db"
},
{
"path": "test/controllers/actual_db_schema/broken_versions_controller_test.rb",
"chars": 4995,
"preview": "# frozen_string_literal: true\n\nrequire_relative \"../../test_helper\"\nrequire_relative \"../../../app/controllers/actual_db"
},
{
"path": "test/controllers/actual_db_schema/migrations_controller_db_storage_test.rb",
"chars": 7464,
"preview": "# frozen_string_literal: true\n\nrequire_relative \"../../test_helper\"\nrequire_relative \"../../../app/controllers/actual_db"
},
{
"path": "test/controllers/actual_db_schema/migrations_controller_test.rb",
"chars": 6882,
"preview": "# frozen_string_literal: true\n\nrequire_relative \"../../test_helper\"\nrequire_relative \"../../../app/controllers/actual_db"
},
{
"path": "test/controllers/actual_db_schema/phantom_migrations_controller_db_storage_test.rb",
"chars": 7317,
"preview": "# frozen_string_literal: true\n\nrequire_relative \"../../test_helper\"\nrequire_relative \"../../../app/controllers/actual_db"
},
{
"path": "test/controllers/actual_db_schema/phantom_migrations_controller_test.rb",
"chars": 6728,
"preview": "# frozen_string_literal: true\n\nrequire_relative \"../../test_helper\"\nrequire_relative \"../../../app/controllers/actual_db"
},
{
"path": "test/controllers/actual_db_schema/schema_controller_db_storage_test.rb",
"chars": 5007,
"preview": "# frozen_string_literal: true\n\nrequire_relative \"../../test_helper\"\nrequire_relative \"../../../app/controllers/actual_db"
},
{
"path": "test/controllers/actual_db_schema/schema_controller_test.rb",
"chars": 6867,
"preview": "# frozen_string_literal: true\n\nrequire_relative \"../../test_helper\"\nrequire_relative \"../../../app/controllers/actual_db"
},
{
"path": "test/dummy_app/config/.keep",
"chars": 1,
"preview": "\n"
},
{
"path": "test/dummy_app/db/migrate/.keep",
"chars": 0,
"preview": ""
},
{
"path": "test/dummy_app/db/migrate_secondary/.keep",
"chars": 0,
"preview": ""
},
{
"path": "test/dummy_app/public/404.html",
"chars": 0,
"preview": ""
},
{
"path": "test/rake_task_console_migrations_db_storage_test.rb",
"chars": 3389,
"preview": "# frozen_string_literal: true\n\nrequire \"test_helper\"\nrequire_relative \"../lib/actual_db_schema/console_migrations\"\n\ndesc"
},
{
"path": "test/rake_task_console_migrations_test.rb",
"chars": 3202,
"preview": "# frozen_string_literal: true\n\nrequire \"test_helper\"\nrequire_relative \"../lib/actual_db_schema/console_migrations\"\n\ndesc"
},
{
"path": "test/rake_task_db_storage_full_test.rb",
"chars": 10721,
"preview": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\ndescribe \"single db (db storage)\" do\n let(:utils) { TestUtils.new"
},
{
"path": "test/rake_task_db_storage_test.rb",
"chars": 1721,
"preview": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\ndescribe \"db storage\" do\n let(:utils) { TestUtils.new }\n\n before"
},
{
"path": "test/rake_task_delete_broken_versions_db_storage_test.rb",
"chars": 7367,
"preview": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\ndescribe \"actual_db_schema:delete_broken_versions (db storage)\" do"
},
{
"path": "test/rake_task_delete_broken_versions_test.rb",
"chars": 6362,
"preview": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\ndescribe \"actual_db_schema:delete_broken_versions\" do\n let(:utils"
},
{
"path": "test/rake_task_git_hooks_install_db_storage_test.rb",
"chars": 4085,
"preview": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\ndescribe \"actual_db_schema:install_git_hooks (db storage)\" do\n le"
},
{
"path": "test/rake_task_git_hooks_install_test.rb",
"chars": 3898,
"preview": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\ndescribe \"actual_db_schema:install_git_hooks\" do\n let(:utils) { T"
},
{
"path": "test/rake_task_multi_tenant_db_storage_test.rb",
"chars": 7000,
"preview": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\ndescribe \"multi-tenant db support (db storage)\" do\n let(:utils) {"
},
{
"path": "test/rake_task_multi_tenant_test.rb",
"chars": 6852,
"preview": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\ndescribe \"multi-tenant db support\" do\n let(:utils) { TestUtils.ne"
},
{
"path": "test/rake_task_schema_diff_db_storage_test.rb",
"chars": 9599,
"preview": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\ndescribe \"actual_db_schema:diff_schema_with_migrations (db storage"
},
{
"path": "test/rake_task_schema_diff_test.rb",
"chars": 17612,
"preview": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\ndescribe \"actual_db_schema:diff_schema_with_migrations\" do\n let(:"
},
{
"path": "test/rake_task_secondary_db_storage_test.rb",
"chars": 5658,
"preview": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\ndescribe \"second db support (db storage)\" do\n let(:utils) do\n "
},
{
"path": "test/rake_task_secondary_test.rb",
"chars": 5453,
"preview": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\ndescribe \"second db support\" do\n let(:utils) do\n TestUtils.new"
},
{
"path": "test/rake_task_test.rb",
"chars": 10517,
"preview": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\ndescribe \"single db\" do\n let(:utils) { TestUtils.new }\n\n before "
},
{
"path": "test/rake_tasks_all_databases_db_storage_test.rb",
"chars": 7120,
"preview": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\ndescribe \"multipe db support (db storage)\" do\n let(:utils) do\n "
},
{
"path": "test/rake_tasks_all_databases_test.rb",
"chars": 6867,
"preview": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\ndescribe \"multipe db support\" do\n let(:utils) do\n TestUtils.ne"
},
{
"path": "test/support/test_utils.rb",
"chars": 7270,
"preview": "# frozen_string_literal: true\n\nclass TestUtils\n attr_accessor :migrations_paths, :migrated_paths, :migration_timestamps"
},
{
"path": "test/test_actual_db_schema.rb",
"chars": 187,
"preview": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\nclass TestActualDbSchema < Minitest::Test\n def test_that_it_has_a"
},
{
"path": "test/test_actual_db_schema_db_storage_test.rb",
"chars": 345,
"preview": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\nclass TestActualDbSchemaDbStorage < Minitest::Test\n def setup\n "
},
{
"path": "test/test_database_filtering.rb",
"chars": 4979,
"preview": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\ndescribe \"database filtering\" do\n let(:utils) do\n TestUtils.ne"
},
{
"path": "test/test_helper.rb",
"chars": 4868,
"preview": "# frozen_string_literal: true\n\n$LOAD_PATH.unshift File.expand_path(\"../lib\", __dir__)\n\n# Clear DATABASE_URL to prevent i"
},
{
"path": "test/test_migration_context.rb",
"chars": 1833,
"preview": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\ndescribe \"ActualDbSchema::MigrationContext#each\" do\n let(:utils) "
}
]
About this extraction
This page contains the full source code of the widefix/actual_db_schema GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 101 files (324.0 KB), approximately 82.5k tokens, and a symbol index with 499 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.