Full Code of widefix/actual_db_schema for AI

main 0ea98c4afbf3 cached
101 files
324.0 KB
82.5k tokens
499 symbols
1 requests
Download .txt
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
================================================
[![Gem Version](https://badge.fury.io/rb/actual_db_schema.svg)](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
Download .txt
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
Download .txt
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": "[![Gem Version](https://badge.fury.io/rb/actual_db_schema.svg)](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.

Copied to clipboard!