[
  {
    "path": ".github/workflows/main.yml",
    "content": "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: Ruby ${{ matrix.ruby }}, Rails ${{ matrix.rails }}\n    strategy:\n      matrix:\n        include:\n          - { ruby: '2.7', rails: '6.0' }\n          - { ruby: '2.7', rails: '6.1' }\n          - { ruby: '3.0', rails: '6.1' }\n          - { ruby: '3.1', rails: '7.0' }\n          - { ruby: '3.2', rails: '7.1' }\n          - { ruby: '3.3', rails: '7.1' }\n          - { ruby: '3.3', rails: 'edge' }\n    env:\n      BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/rails.${{ matrix.rails }}.gemfile\n    steps:\n    - uses: actions/checkout@v2\n    - name: Install SQLite3 Development Libraries\n      run: sudo apt-get update && sudo apt-get install -y libsqlite3-dev docker-compose\n    - name: Set up Ruby\n      uses: ruby/setup-ruby@v1\n      with:\n        ruby-version: ${{ matrix.ruby }}\n        bundler-cache: true\n    - name: Run Tests with All Adapters\n      run: bundle exec rake test:all\n\n  rubocop:\n    runs-on: ubuntu-latest\n    name: Rubocop\n    steps:\n    - uses: actions/checkout@v2\n    - uses: ruby/setup-ruby@v1\n      with:\n        ruby-version: 2.7\n        bundler-cache: true\n    - run: bundle exec rubocop\n"
  },
  {
    "path": ".gitignore",
    "content": "/.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/test/dummy_app/db/**/*.rb\n/test/dummy_app/db/structure.sql\n/test/dummy_app/config/database.yml\n.ruby-version\n.ruby-gemset\n/gemfiles/*.gemfile.lock\n"
  },
  {
    "path": ".rubocop.yml",
    "content": "AllCops:\n  TargetRubyVersion: 2.7\n  Exclude:\n    - gemfiles/*\n    - test/dummy_app/**/*\n    - vendor/**/*\n\nStyle/StringLiterals:\n  Enabled: true\n  EnforcedStyle: double_quotes\n\nStyle/StringLiteralsInInterpolation:\n  Enabled: true\n  EnforcedStyle: double_quotes\n\nLayout/LineLength:\n  Max: 120\n\nMetrics/BlockLength:\n  Exclude:\n    - test/**/*\n    - actual_db_schema.gemspec\n\nMetrics/MethodLength:\n  Max: 15\n  Exclude:\n    -  test/**/*\n\nMetrics/ClassLength:\n  Enabled: false\n\nMetrics/ModuleLength:\n  Enabled: false\n\nMetrics/AbcSize:\n  Max: 25\n\nMetrics/CyclomaticComplexity:\n  Max: 10\n\nMetrics/PerceivedComplexity:\n  Max: 10\n\nMetrics/AbcSize:\n  Enabled: false\n"
  },
  {
    "path": "Appraisals",
    "content": "# 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 \"activerecord\", \"~> #{version}.0\"\n    gem \"activesupport\", \"~> #{version}.0\"\n  end\nend\n\nappraise \"rails.edge\" do\n  gem \"rails\", \">= 7.2.0.beta\"\n  gem \"activerecord\", \">= 7.2.0.beta\"\n  gem \"activesupport\", \">= 7.2.0.beta\"\nend\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "## [0.9.1] - 2026-02-25\n\n- Support schema diffs for `structure.sql`\n- Add an option to exclude specific databases from the gem's visibility scope\n- Fix a crash when the database is not available at application startup\n- Add instrumentation tooling to track stats about rolled-back phantom migrations\n\n## [0.9.0] - 2026-01-27\n- 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).\n\n## [0.8.6] - 2025-05-21\n- Fix gem installtion with git hooks\n- Update README\n\n## [0.8.5] - 2025-04-10\n\n- Fix the gem working on projects without git\n\n## [0.8.4] - 2025-03-20\n\n- Fix initializer file that can break other bundle groups that development\n- Use prism gem instead of parser for Ruby 3.4 compatibility\n\n## [0.8.3] - 2025-03-03\n\n- View Schema with Migration Annotations in the UI\n- Clean Up Broken Migrations\n- Filter Migrations in the UI\n- Customize Your Migrated Folder Location\n\n## [0.8.2] - 2025-02-06\n\n- Show migration name in the schema.rb diff that caused the change\n- Easy way to run DDL migration methods in Rails console\n\n## [0.8.1] - 2025-01-15\n\n- Support for multiple database schemas, ensuring compatibility with multi-tenant applications using the apartment gem or similar solutions\n- DSL for configuring the gem, simplifying setup and customization\n- Rake task added to initialize the gem\n- Improved the post-checkout git hook to run only when switching branches, reducing unnecessary executions during file checkouts\n- Fixed the changelog link in the gemspec, ensuring Rubygems points to the correct file and the link works\n\n## [0.8.0] - 2024-12-30\n- Enhanced Console Visibility: Automatically rolled-back phantom migrations now provide clearer and more visible logs in the console\n- 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\n- Temporary Folder Cleanup: Rolled-back phantom migrations are now automatically deleted from the temporary folder after rollback\n- 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\n\n## [0.7.9] - 2024-09-07\n- Don't stop if a phantom migration rollback fails\n- Improve failed rollback of phantom migrations report\n\n## [0.7.8] - 2024-08-07\n- Make UI working without assets pipeline\n\n## [0.7.7] - 2024-07-22\n- Unlock compatibility with Rails versions earlier than 6.0\n\n## [0.7.6] - 2024-07-22\n- Added UI\n- Added environment variable `ACTUAL_DB_SCHEMA_UI_ENABLED` to enable/disable the UI in specific environments\n- Added configuration option `ActualDbSchema.config[:ui_enabled]` to enable/disable the UI in specific environments\n\n## [0.7.5] - 2024-06-20\n- Added db:rollback_migrations:manual task to manually rolls back phantom migrations one by one\n\n## [0.7.4] - 2024-06-06\n- Rails 7.2 support added\n- Rails 6.0 support added\n\n## [0.7.3] - 2024-04-06\n- add multipe databases support\n\n## [0.7.2] - 2024-03-30\n- update title and description in Rubygems\n\n## [0.7.1] - 2024-03-19\n\n- add csv as a dependency since Ruby 3.3 has removed it from the standard library\n\n## [0.7.0] - 2024-01-18\n\n- db:phantom_migrations displays the branch in which the phantion migration was run\n\n## [0.6.0] - 2024-01-03\n\n- Added db:phantom_migrations task to display phantom migrations\n- Updated README\n\n## [0.5.0] - 2023-11-06\n\n- Rails 7.1 support added\n\n## [0.4.0] - 2023-07-05\n\n- rollback migrations in the reversed order\n\n## [0.3.0] - 2023-01-23\n\n- add Rails 6 and older support\n\n## [0.2.0] - 2022-10-19\n\n- Catch exceptions about irreversible migrations and show a warning\n- Namespace all patches into gem module\n- Fix typo in a module name with a patch\n- Use guard clause\n\n## [0.1.0] - 2022-10-16\n\n- Initial release\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe 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.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our community include:\n\n* Demonstrating empathy and kindness toward other people\n* Being respectful of differing opinions, viewpoints, and experiences\n* Giving and gracefully accepting constructive feedback\n* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience\n* Focusing on what is best not just for us as individuals, but for the overall community\n\nExamples of unacceptable behavior include:\n\n* The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\n* Trolling, insulting or derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity 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.\n\nCommunity 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.\n\n## Scope\n\nThis 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.\n\n## Enforcement\n\nInstances 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.\n\nAll community leaders are obligated to respect the privacy and security of the reporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.\n\n**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.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series of actions.\n\n**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.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.\n\n**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.\n\n### 4. Permanent Ban\n\n**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.\n\n**Consequence**: A permanent ban from any sort of public interaction within the community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,\navailable at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.\n\nCommunity Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see the FAQ at\nhttps://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.\n"
  },
  {
    "path": "Gemfile",
    "content": "# frozen_string_literal: true\n\nsource \"https://rubygems.org\"\n\n# Specify your gem's dependencies in actual_db_schema.gemspec\ngemspec\n\ngem \"activerecord\", \"~> 7.1.0\"\ngem \"activesupport\", \"~> 7.1.0\"\ngem \"minitest\", \"~> 5.0\"\ngem \"rake\"\ngem \"rubocop\", \"~> 1.21\"\n"
  },
  {
    "path": "LICENSE.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2022 Andrei Kaleshka\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "[![Gem Version](https://badge.fury.io/rb/actual_db_schema.svg)](https://badge.fury.io/rb/actual_db_schema)\n\n# ActualDbSchema\n\n**Stop database headaches when switching Git branches in Rails**\n\nKeep your database schema perfectly synchronized across Git branches, eliminate broken tests and schema conflicts, and save wasted hours on phantom migrations.\n\n## 🚀 What You Get\n\n- **Zero Manual Work**: Switch branches freely - phantom migrations roll back automatically\n- **No More Schema Conflicts**: Clean `schema.rb`/`structure.sql` diffs every time, no irrelevant changes\n- **Error Prevention**: Eliminates `ActiveRecord::NotNullViolation` and similar errors when switching branches\n- **Time Savings**: Stop hunting down which branch has the problematic migration\n- **Team Productivity**: Everyone stays focused on coding, not database maintenance\n- **Staging/Sandbox Sync**: Keep staging and sandbox databases aligned with your current branch code\n- **Visual Management**: Web UI to view and manage migrations across all databases\n\n<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\" />\n\nAnd you get all of that with **zero** changes to your workflow!\n\n## 🎯 The Problem This Solves\n\n**Before ActualDbSchema:**\n1. Work on Branch A → Add migration → Run migration\n2. Switch to Branch B → Code breaks with database errors\n3. Manually find and rollback the \"phantom\" migration\n4. Deal with irrelevant `schema.rb` diffs\n5. Repeat this tedious process constantly\n\n**After ActualDbSchema:**\n1. Work on any branch → Add migrations as usual\n2. Switch branches freely → Everything just works\n3. Focus on building features, not fixing database issues\n\n## 🌟 Complete Feature Set\n\n### Core Migration Management\n- **Phantom Migration Detection**: Automatically identifies migrations from other branches\n- **Smart Rollback**: Rolls back phantom migrations in correct dependency order\n- **Irreversible Migration Handling**: Safely handles and reports irreversible migrations\n- **Multi-Database Support**: Works seamlessly with multiple database configurations\n- **Schema Format Agnostic**: Supports both `schema.rb` and `structure.sql`\n\n### Automation & Git Integration\n- **Automatic Rollback on Migration**: Phantom migrations roll back when running `db:migrate`\n- **Git Hook Integration**: Optional automatic rollback when switching branches\n- **Zero Configuration**: Works out of the box with sensible defaults\n- **Custom Migration Storage**: Configurable location for storing executed migrations\n\n### Web Interface & Management\n- **Migration Dashboard**: Visual overview of all migrations across databases\n- **Phantom Migration Browser**: Easy-to-use interface for viewing phantom migrations\n- **One-Click Rollback**: Rollback individual or all phantom migrations via web UI\n- **Broken Version Cleanup**: Identify and remove orphaned migration records\n- **Schema Diff Viewer**: Visual diff of schema changes with migration annotations\n\n### Developer Tools\n- **Console Migrations**: Run migration commands directly in Rails console\n- **Schema Diff Analysis**: Annotated diffs showing which migrations caused changes\n- **Migration Search & Filter**: Find specific migrations across all databases\n- **Detailed Migration Info**: View migration status, branch, and database information\n\n### Team & Environment Support\n- **Multi-Tenant Compatible**: Works with apartment gem and similar multi-tenant setups\n- **Environment Flexibility**: Enable/disable features per environment\n- **Team Synchronization**: Keeps all team members' databases in sync\n- **CI/CD Friendly**: No interference with deployment pipelines\n\n### Manual Control Options\n- **Manual Rollback Mode**: Disable automatic rollback for full manual control\n- **Selective Rollback**: Choose which phantom migrations to rollback\n- **Interactive Mode**: Step-by-step confirmation for each rollback operation\n- **Rake Task Integration**: Full set of rake tasks for command-line management\n\n## ⚡ Quick Start\n\nAdd to your Gemfile:\n\n```ruby\ngroup :development do\n  gem \"actual_db_schema\"\nend\n```\n\nInstall and configure:\n\n```sh\nbundle install\nrails actual_db_schema:install\n```\n\nThat's it! Now just run `rails db:migrate` as usual - phantom migrations roll back automatically.\n\n## 🔧 How It Works\n\nThis 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*.\n\nThe *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).\n\nTherefore, 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.\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngroup :development do\n  gem \"actual_db_schema\"\nend\n```\n\nAnd then execute:\n\n    $ bundle install\n\nIf 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/).\n\nNext, generate your ActualDbSchema initializer file by running:\n\n```sh\nrails actual_db_schema:install\n```\n\nThis 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.\n\nFor more details on the available configuration options, see the sections below.\n\n## Usage\n\nJust run `rails db:migrate` inside the current branch. It will roll back all phantom migrations for all configured databases in your `database.yml.`\n\n> [!WARNING]\n> 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.\n\nThe gem offers the following rake tasks that can be manually run according to your preferences:\n- `rails db:rollback_branches` - run it to manually rolls back phantom migrations.\n- `rails db:rollback_branches:manual` - run it to manually rolls back phantom migrations one by one.\n- `rails db:phantom_migrations` - displays a list of phantom migrations.\n\n## 🎛️ Configuration Options\n\nBy 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:\n\n### 1. Using Environment Variable\n\nSet the environment variable `ACTUAL_DB_SCHEMA_MIGRATED_FOLDER` to your desired folder path:\n\n```sh\nexport ACTUAL_DB_SCHEMA_MIGRATED_FOLDER=\"custom/migrated\"\n```\n\n### 2. Using Initializer\nAdd the following line to your initializer file (`config/initializers/actual_db_schema.rb`):\n\n```ruby\nconfig.migrated_folder = Rails.root.join(\"custom\", \"migrated\")\n```\n\n### 3. Store migrations in the database\n\nIf you want to share executed migrations across environments (e.g., staging or sandboxes),\nstore them in the main database instead of the local filesystem:\n\n```ruby\nconfig.migrations_storage = :db\n```\n\nOr via environment variable:\n\n```sh\nexport ACTUAL_DB_SCHEMA_MIGRATIONS_STORAGE=\"db\"\n```\n\nIf both are set, the initializer setting (`config.migrations_storage`) takes precedence.\n\n## 🌐 Web Interface\n\nAccess the migration management UI at:\n```\nhttp://localhost:3000/rails/phantom_migrations\n```\n\nView and manage:\n- **Migration Overview**: See all executed migrations with their status, branch, and database\n- **Phantom Migrations**: Identify migrations from other branches that need rollback\n- **Migration Source Code**: Browse the source code of every migration ever run (including the phantom ones)\n- **One-Click Actions**: Rollback or migrate individual migrations directly from the UI\n- **Broken Versions**: Detect and clean up orphaned migration records safely\n- **Schema Diffs**: Visual diff of schema changes annotated with their source migrations\n\n## UI options\n\nBy 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:\n\n### 1. Using Environment Variable\n\nSet the environment variable `ACTUAL_DB_SCHEMA_UI_ENABLED` to `true`:\n\n```sh\nexport ACTUAL_DB_SCHEMA_UI_ENABLED=true\n```\n\n### 2. Using Initializer\nAdd the following line to your initializer file (`config/initializers/actual_db_schema.rb`):\n\n```ruby\nconfig.ui_enabled = true\n```\n\n> With this option, the UI can be disabled for all environments or be enabled in specific ones.\n\n## Disabling Automatic Rollback\n\nBy default, the automatic rollback of migrations is enabled. If you prefer to perform manual rollbacks, you can disable the automatic rollback in two ways:\n\n### 1. Using Environment Variable\n\nSet the environment variable `ACTUAL_DB_SCHEMA_AUTO_ROLLBACK_DISABLED` to `true`:\n\n```sh\nexport ACTUAL_DB_SCHEMA_AUTO_ROLLBACK_DISABLED=true\n```\n\n### 2. Using Initializer\nAdd the following line to your initializer file (`config/initializers/actual_db_schema.rb`):\n\n```ruby\nconfig.auto_rollback_disabled = true\n```\n\n## Rollback Instrumentation\n\nActualDbSchema emits an `ActiveSupport::Notifications` event for each successful phantom rollback:\n\n- Event name: `rollback_migration.actual_db_schema`\n- Event is always emitted when a phantom rollback succeeds\n\n### Event payload\n\n| Field | Description |\n|-------|-------------|\n| `version` | Migration version that was rolled back |\n| `name` | Migration class name |\n| `database` | Current database name from Active Record config |\n| `schema` | Tenant schema name (or `nil` for default schema) |\n| `branch` | Branch associated with the migration metadata |\n| `manual_mode` | Whether rollback was run in manual mode |\n\n### Subscribing to rollback events\n\nYou can subscribe to rollback events in your initializer to track statistics or perform custom actions:\n\n```ruby\n# config/initializers/actual_db_schema.rb\nActiveSupport::Notifications.subscribe(ActualDbSchema::Instrumentation::ROLLBACK_EVENT) do |_name, _start, _finish, _id, payload|\n  ActualDbSchema::RollbackStatsRepository.record(payload)\nend\n```\n\nThe `RollbackStatsRepository` persists rollback events to a database table (`actual_db_schema_rollback_events`) that is automatically excluded from schema dumps.\n\nRead aggregated stats at runtime:\n\n```ruby\nActualDbSchema::RollbackStatsRepository.stats\n# => { total: 3, by_database: { \"primary\" => 3 }, by_schema: { \"default\" => 3 }, by_branch: { \"main\" => 3 } }\n\nActualDbSchema::RollbackStatsRepository.total_rollbacks\n# => 3\n\nActualDbSchema::RollbackStatsRepository.reset!\n# Clears all recorded stats\n```\n\n## Automatic Phantom Migration Rollback On Branch Switch\n\nBy 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:\n\n### 1. Using Environment Variable\n\nSet the environment variable `ACTUAL_DB_SCHEMA_GIT_HOOKS_ENABLED` to `true`:\n\n```sh\nexport ACTUAL_DB_SCHEMA_GIT_HOOKS_ENABLED=true\n```\n\n### 2. Using Initializer\nAdd the following line to your initializer file (`config/initializers/actual_db_schema.rb`):\n\n```ruby\nconfig.git_hooks_enabled = true\n```\n\n### Installing the Post-Checkout Hook\nAfter enabling Git hooks in your configuration, run the rake task to install the post-checkout hook:\n\n```sh\nrake actual_db_schema:install_git_hooks\n```\n\nThis task will prompt you to choose one of the three options:\n\n1. Rollback phantom migrations with `db:rollback_branches`\n2. Migrate up to the latest schema with `db:migrate`\n3. Skip installing git hook\n\nBased on your selection, a post-checkout hook will be installed or updated in your `.git/hooks` folder.\n\n## Excluding Databases from Processing\n\n**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.\n\n### Why You Might Need This\n\nModern 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:\n\n- **Solid Queue** (Rails 8 default job backend)\n- **Solid Cable** (WebSocket connections)\n- **Solid Cache** (caching infrastructure)\n\n### Method 1: Using `excluded_databases` Configuration\n\nExplicitly exclude databases by name in your initializer:\n\n```ruby\n# config/initializers/actual_db_schema.rb\nActualDbSchema.configure do |config|\n  config.excluded_databases = [:queue, :cable, :cache]\nend\n```\n\n### Method 2: Using Environment Variable\n\nSet the environment variable `ACTUAL_DB_SCHEMA_EXCLUDED_DATABASES` with a comma-separated list:\n\n```sh\nexport ACTUAL_DB_SCHEMA_EXCLUDED_DATABASES=\"queue,cable,cache\"\n```\n\n**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.\n\n## Multi-Tenancy Support\n\nIf 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`):\n\n```ruby\nconfig.multi_tenant_schemas = -> { # list of all active schemas }\n```\n\n### Example:\n\n```ruby\nconfig.multi_tenant_schemas = -> { [\"public\", \"tenant1\", \"tenant2\"] }\n```\n\n## Schema Diff with Migration Annotations\n\nIf `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.\n\nBy default, the task uses `db/schema.rb` and `db/migrate` as the schema and migrations paths. You can also provide custom paths as arguments.\n\nAlternatively, 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.\n\n### Usage\n\nRun the task with default paths:\n```sh\nrake actual_db_schema:diff_schema_with_migrations\n```\n\nRun the task with custom paths:\n```sh\nrake actual_db_schema:diff_schema_with_migrations[path/to/custom_schema.rb, path/to/custom_migrations]\n```\n\n## Console Migrations\n\nSometimes, 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.\n\nBy default, Console Migrations is disabled. You can enable it in two ways:\n\n### 1. Using Environment Variable\n\nSet the environment variable `ACTUAL_DB_SCHEMA_CONSOLE_MIGRATIONS_ENABLED` to `true`:\n\n```sh\nexport ACTUAL_DB_SCHEMA_CONSOLE_MIGRATIONS_ENABLED=true\n```\n\n### 2. Using Initializer\n\nAdd the following line to your initializer file (`config/initializers/actual_db_schema.rb`):\n\n```ruby\nconfig.console_migrations_enabled = true\n```\n\n### Usage\n\nOnce enabled, you can run migration commands directly in the Rails console:\n\n```ruby\n# Create a new table\ncreate_table :posts do |t|\n  t.string :title\nend\n\n# Add a column\nadd_column :users, :age, :integer\n\n# Remove an index\nremove_index :users, :email\n\n# Rename a column\nrename_column :users, :username, :handle\n```\n\n## Delete Broken Migrations\n\nA 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.\n\nYou can delete broken migrations using either of the following methods:\n\n### 1. Using the UI\n\nNavigate to the following URL in your web browser:\n```\nhttp://localhost:3000/rails/broken_versions\n```\n\nThis page lists all broken versions and provides an option to delete them.\n\n### 2. Using a Rake Task\n\nTo delete all broken migrations, run:\n```sh\nrake actual_db_schema:delete_broken_versions\n```\n\nTo delete specific migrations, pass the migration version(s) and optionally a database:\n```sh\nrake actual_db_schema:delete_broken_versions[<version>, <version>]\n```\n\n- `<version>` – The migration version(s) to delete (space-separated if multiple).\n- `<database>` (optional) – Specify a database if using multiple databases.\n\n#### Examples:\n\n```sh\n# Delete all broken migrations\nrake actual_db_schema:delete_broken_versions\n\n# Delete specific migrations\nrake actual_db_schema:delete_broken_versions[\"20250224103352 20250224103358\"]\n\n# Delete specific migrations from a specific database\nrake actual_db_schema:delete_broken_versions[\"20250224103352 20250224103358\", \"primary\"]\n```\n\n## 🏗️ Development\n\nAfter 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.\n\nTo install this gem onto your local machine, run `bundle exec rake install`.\n\nTo release a new version do the following in the order:\n\n- update the version number in `version.rb`;\n- update the CHANGELOG;\n- `bundle install` to update `Gemfile.lock`;\n- make the commit and push;\n- 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);\n- [announce the new release on GitHub](https://github.com/widefix/actual_db_schema/releases);\n- close the milestone on GitHub.\n\n### Running Tests with Specific Rails Versions\n\nThe following versions can be specifically tested using Appraisal\n- 6.0\n- 6.1\n- 7.0\n- 7.1\n- edge\n\nTo run tests with a specific version of Rails using Appraisal:\n- Run all tests with Rails 6.0:\n  ```sh\n  bundle exec appraisal rails.6.0 rake test\n  ```\n- Run tests for a specific file:\n  ```sh\n  bundle exec appraisal rails.6.0 rake test TEST=test/rake_task_test.rb\n  ```\n- Run a specific test:\n  ```sh\n  bundle exec appraisal rails.6.0 rake test TEST=test/rake_task_test.rb TESTOPTS=\"--name=/db::db:rollback_branches#test_0003_keeps/\"\n  ```\n\nBy default, `rake test` runs tests using `SQLite3`. To explicitly run tests with `SQLite3`, `PostgreSQL`, or `MySQL`, you can use the following tasks:\n- Run tests with `SQLite3`:\n  ```sh\n  bundle exec rake test:sqlite3\n  ```\n- Run tests with `PostgreSQL` (requires Docker):\n  ```sh\n  bundle exec rake test:postgresql\n  ```\n- Run tests with `MySQL` (requires Docker):\n  ```sh\n  bundle exec rake test:mysql2\n  ```\n- Run tests for all supported adapters:\n  ```sh\n  bundle exec rake test:all\n  ```\n\n## Contributing\n\nBug 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).\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n\n## Code of Conduct\n\nEveryone 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).\n"
  },
  {
    "path": "Rakefile",
    "content": "# frozen_string_literal: true\n\nrequire \"bundler/gem_tasks\"\nrequire \"rake/testtask\"\n\nload \"lib/tasks/test.rake\"\n\nRake::TestTask.new(:test) do |t|\n  t.libs << \"test\"\n  t.libs << \"lib\"\n  t.test_files = FileList[\"test/**/test_*.rb\", \"test/**/*_test.rb\"]\nend\n\nrequire \"rubocop/rake_task\"\n\nRuboCop::RakeTask.new\n\ntask default: %i[test]\n"
  },
  {
    "path": "actual_db_schema.gemspec",
    "content": "# frozen_string_literal: true\n\nrequire_relative \"lib/actual_db_schema/version\"\n\nGem::Specification.new do |spec|\n  spec.name = \"actual_db_schema\"\n  spec.version = ActualDbSchema::VERSION\n  spec.authors = [\"Andrei Kaleshka\"]\n  spec.email = [\"ka8725@gmail.com\"]\n\n  spec.summary = \"Keep DB schema in sync across branches effortlessly.\"\n  spec.description = <<~DESC\n    Keep your DB schema in sync across all branches effortlessly.\n    Install once, then use `rails db:migrate` normally — the gem handles phantom migration rollback automatically, eliminating schema conflicts and inconsistent database states.\n    Stop wasting hours on DB maintenance locally, CI, staging/sandbox, or even production.\n  DESC\n  spec.homepage = \"https://blog.widefix.com/actual-db-schema/\"\n  spec.license = \"MIT\"\n  spec.required_ruby_version = \">= 2.7.0\"\n\n  # spec.metadata[\"allowed_push_host\"] = \"TODO: Set to your gem server 'https://example.com'\"\n\n  spec.metadata[\"homepage_uri\"] = spec.homepage\n  spec.metadata[\"source_code_uri\"] = \"https://github.com/widefix/actual_db_schema\"\n  spec.metadata[\"changelog_uri\"] = \"https://github.com/widefix/actual_db_schema/blob/main/CHANGELOG.md\"\n\n  # Specify which files should be added to the gem when it is released.\n  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.\n  spec.files = Dir.chdir(File.expand_path(__dir__)) do\n    `git ls-files -z`.split(\"\\x0\").reject do |f|\n      (f == __FILE__) || f.match(%r{\\A(?:(?:bin|test|spec|features)/|\\.(?:git|travis|circleci)|appveyor)})\n    end\n  end\n  spec.bindir = \"exe\"\n  spec.executables = spec.files.grep(%r{\\Aexe/}) { |f| File.basename(f) }\n  spec.require_paths = [\"lib\"]\n\n  # Uncomment to register a new dependency of your gem\n  spec.add_runtime_dependency \"activerecord\"\n  spec.add_runtime_dependency \"activesupport\"\n  spec.add_runtime_dependency \"ast\"\n  spec.add_runtime_dependency \"csv\"\n  spec.add_runtime_dependency \"parser\"\n  spec.add_runtime_dependency \"prism\"\n\n  spec.add_development_dependency \"appraisal\"\n  spec.add_development_dependency \"debug\"\n  spec.add_development_dependency \"rails\"\n  spec.add_development_dependency \"sqlite3\"\n\n  spec.post_install_message = <<~MSG\n    Thank you for installing ActualDbSchema!\n\n    Next steps:\n    1. Run `rake actual_db_schema:install` to generate the initializer file and install\n       the post-checkout Git hook for automatic phantom migration rollback when switching branches.\n    2. Or, if you prefer environment variables, skip this step.\n\n    For more information, see the README.\n\n  MSG\n\n  # For more information and examples about making a new gem, check out our\n  # guide at: https://bundler.io/guides/creating_gem.html\nend\n"
  },
  {
    "path": "app/controllers/actual_db_schema/broken_versions_controller.rb",
    "content": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n  # Controller for managing broken migration versions.\n  class BrokenVersionsController < ActionController::Base\n    protect_from_forgery with: :exception\n    skip_before_action :verify_authenticity_token\n\n    def index; end\n\n    def delete\n      handle_delete(params[:id], params[:database])\n      redirect_to broken_versions_path\n    end\n\n    def delete_all\n      handle_delete_all\n      redirect_to broken_versions_path\n    end\n\n    private\n\n    def handle_delete(id, database)\n      ActualDbSchema::Migration.instance.delete(id, database)\n      flash[:notice] = \"Migration #{id} was successfully deleted.\"\n    rescue StandardError => e\n      flash[:alert] = e.message\n    end\n\n    def handle_delete_all\n      ActualDbSchema::Migration.instance.delete_all\n      flash[:notice] = \"All broken versions were successfully deleted.\"\n    rescue StandardError => e\n      flash[:alert] = e.message\n    end\n\n    helper_method def broken_versions\n      @broken_versions ||= ActualDbSchema::Migration.instance.broken_versions\n    end\n  end\nend\n"
  },
  {
    "path": "app/controllers/actual_db_schema/migrations_controller.rb",
    "content": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n  # Controller to display the list of migrations for each database connection.\n  class MigrationsController < ActionController::Base\n    protect_from_forgery with: :exception\n    skip_before_action :verify_authenticity_token\n\n    def index; end\n\n    def show\n      render file: \"#{Rails.root}/public/404.html\", layout: false, status: :not_found unless migration\n    end\n\n    def rollback\n      handle_rollback(params[:id], params[:database])\n      redirect_to migrations_path\n    end\n\n    def migrate\n      handle_migrate(params[:id], params[:database])\n      redirect_to migrations_path\n    end\n\n    private\n\n    def handle_rollback(id, database)\n      ActualDbSchema::Migration.instance.rollback(id, database)\n      flash[:notice] = \"Migration #{id} was successfully rolled back.\"\n    rescue StandardError => e\n      flash[:alert] = e.message\n    end\n\n    def handle_migrate(id, database)\n      ActualDbSchema::Migration.instance.migrate(id, database)\n      flash[:notice] = \"Migration #{id} was successfully migrated.\"\n    rescue StandardError => e\n      flash[:alert] = e.message\n    end\n\n    helper_method def migrations\n      @migrations ||= ActualDbSchema::Migration.instance.all\n      query = params[:query].to_s.strip.downcase\n\n      return @migrations if query.blank?\n\n      @migrations.select do |migration|\n        file_name_matches = migration[:filename].include?(query)\n        content_matches = begin\n          File.read(migration[:filename]).downcase.include?(query)\n        rescue StandardError\n          false\n        end\n\n        file_name_matches || content_matches\n      end\n    end\n\n    helper_method def migration\n      @migration ||= ActualDbSchema::Migration.instance.find(params[:id], params[:database])\n    end\n  end\nend\n"
  },
  {
    "path": "app/controllers/actual_db_schema/phantom_migrations_controller.rb",
    "content": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n  # Controller to display the list of phantom migrations for each database connection.\n  class PhantomMigrationsController < ActionController::Base\n    protect_from_forgery with: :exception\n    skip_before_action :verify_authenticity_token\n\n    def index; end\n\n    def show\n      render file: \"#{Rails.root}/public/404.html\", layout: false, status: :not_found unless phantom_migration\n    end\n\n    def rollback\n      handle_rollback(params[:id], params[:database])\n      redirect_to phantom_migrations_path\n    end\n\n    def rollback_all\n      handle_rollback_all\n      redirect_to phantom_migrations_path\n    end\n\n    private\n\n    def handle_rollback(id, database)\n      ActualDbSchema::Migration.instance.rollback(id, database)\n      flash[:notice] = \"Migration #{id} was successfully rolled back.\"\n    rescue StandardError => e\n      flash[:alert] = e.message\n    end\n\n    def handle_rollback_all\n      ActualDbSchema::Migration.instance.rollback_all\n      flash[:notice] = \"Migrations was successfully rolled back.\"\n    rescue StandardError => e\n      flash[:alert] = e.message\n    end\n\n    helper_method def phantom_migrations\n      @phantom_migrations ||= ActualDbSchema::Migration.instance.all_phantom\n    end\n\n    helper_method def phantom_migration\n      @phantom_migration ||= ActualDbSchema::Migration.instance.find(params[:id], params[:database])\n    end\n  end\nend\n"
  },
  {
    "path": "app/controllers/actual_db_schema/schema_controller.rb",
    "content": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n  # Controller to display the database schema diff.\n  class SchemaController < ActionController::Base\n    protect_from_forgery with: :exception\n    skip_before_action :verify_authenticity_token\n\n    def index; end\n\n    private\n\n    helper_method def schema_diff_html\n      schema_path = Rails.configuration.active_record.schema_format == :sql ? \"./db/structure.sql\" : \"./db/schema.rb\"\n      schema_diff = ActualDbSchema::SchemaDiffHtml.new(schema_path, \"db/migrate\")\n      schema_diff.render_html(params[:table])\n    end\n  end\nend\n"
  },
  {
    "path": "app/views/actual_db_schema/broken_versions/index.html.erb",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>Broken Versions</title>\n    <%= render partial: 'actual_db_schema/shared/js' %>\n    <%= render partial: 'actual_db_schema/shared/style' %>\n  </head>\n  <body>\n    <div>\n      <% flash.each do |key, message| %>\n        <div class=\"flash <%= key %>\"><%= message %></div>\n      <% end %>\n      <h2>Broken Versions</h2>\n      <p>\n        These are versions that were migrated in the database, but the corresponding migration file is missing.  \n        You can safely delete them from the database to clean up.\n      </p>\n      <div class=\"top-buttons\">\n        <%= link_to 'All Migrations', migrations_path, class: \"top-button\" %>\n        <% if broken_versions.present? %>\n          <%= button_to '✖ Delete all',\n                        delete_all_broken_versions_path,\n                        method: :post,\n                        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?' },\n                        class: 'button migration-action' %>\n        <% end %>\n      </div>\n      <% if broken_versions.present? %>\n        <table>\n          <thead>\n            <tr>\n              <th>Status</th>\n              <th>Migration ID</th>\n              <th>Branch</th>\n              <th>Database</th>\n              <th>Actions</th>\n            </tr>\n          </thead>\n          <tbody>\n            <% broken_versions.each do |version| %>\n              <tr class=\"migration-row phantom\">\n                <td><%= version[:status] %></td>\n                <td><%= version[:version] %></td>\n                <td><%= version[:branch] %></td>\n                <td><%= version[:database] %></td>\n                <td>\n                  <div class='button-container'>\n                    <%= button_to '✖ Delete',\n                                  delete_broken_version_path(id: version[:version], database: version[:database]),\n                                  method: :post,\n                                  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?' },\n                                  class: 'button migration-action' %>\n                  </div>\n                </td>\n              </tr>\n            <% end %>\n          </tbody>\n        </table>\n      <% else %>\n        <p>No broken versions found.</p>\n      <% end %>\n    </div>\n  </body>\n</html>\n"
  },
  {
    "path": "app/views/actual_db_schema/migrations/index.html.erb",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>Migrations</title>\n    <%= render partial: 'actual_db_schema/shared/js' %>\n    <%= render partial: 'actual_db_schema/shared/style' %>\n  </head>\n  <body>\n    <div>\n      <% flash.each do |key, message| %>\n        <div class=\"flash <%= key %>\"><%= message %></div>\n      <% end %>\n      <h2>Migrations</h2>\n      <p>\n        <span style=\"background-color: #ffe6e6; padding: 0 5px;\">Red rows</span> represent phantom migrations.\n      </p>\n      <div class=\"container\">\n        <div class=\"top-controls\">\n          <div class=\"top-buttons\">\n            <%= link_to 'Phantom Migrations', phantom_migrations_path, class: \"top-button\" %>\n            <%= link_to 'Broken Versions', broken_versions_path, class: \"top-button\" %>\n            <%= link_to 'View Schema', schema_path, class: \"top-button\" %>\n          </div>\n          <div class=\"top-search\">\n            <%= form_tag migrations_path, method: :get, class: \"search-form\" do %>\n              <span class=\"search-icon\">🔍</span>\n              <%= text_field_tag :query, params[:query], placeholder: \"Search migrations by name or content\", class: \"search-input\" %>\n            <% end %>\n          </div>\n        </div>\n        <% if migrations.present? %>\n          <table>\n            <thead>\n              <tr>\n                <th>Status</th>\n                <th>Migration ID</th>\n                <th>Name</th>\n                <th>Branch</th>\n                <th>Database</th>\n                <th>Actions</th>\n              </tr>\n            </thead>\n            <tbody>\n              <% migrations.each do |migration| %>\n                <tr class=\"migration-row <%= migration[:phantom] ? 'phantom' : 'normal' %>\">\n                  <td><%= migration[:status] %></td>\n                  <td><%= migration[:version] %></td>\n                  <td>\n                    <div class=\"truncate-text\" title=\"<%= migration[:name] %>\">\n                      <%= migration[:name] %>\n                    </div>\n                  </td>\n                  <td><%= migration[:branch] %></td>\n                  <td><%= migration[:database] %></td>\n                  <td>\n                    <div class='button-container'>\n                      <%= link_to '👁 Show',\n                                  migration_path(id: migration[:version], database: migration[:database]),\n                                  class: 'button' %>\n                      <%= button_to '⎌ Rollback',\n                                    rollback_migration_path(id: migration[:version], database: migration[:database]),\n                                    method: :post,\n                                    class: 'button migration-action',\n                                    style: ('display: none;' if migration[:status] == \"down\") %>\n                      <%= button_to '⬆ Migrate',\n                                    migrate_migration_path(id: migration[:version], database: migration[:database]),\n                                    method: :post,\n                                    class: 'button migration-action',\n                                    style: ('display: none;' if migration[:status] == \"up\" || migration[:phantom]) %>\n                    </div>\n                  </td>\n                </tr>\n              <% end %>\n            </tbody>\n          </table>\n        <% else %>\n          <p>No migrations found.</p>\n        <% end %>\n      </div>\n    </div>\n  </body>\n</html>\n"
  },
  {
    "path": "app/views/actual_db_schema/migrations/show.html.erb",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>Migration Details</title>\n    <%= render partial: 'actual_db_schema/shared/js' %>\n    <%= render partial: 'actual_db_schema/shared/style' %>\n  </head>\n  <body>\n    <div>\n      <% flash.each do |key, message| %>\n        <div class=\"flash <%= key %>\"><%= message %></div>\n      <% end %>\n      <h2>Migration <%= migration[:name] %> Details</h2>\n      <table>\n        <tbody>\n          <tr>\n            <th>Status</th>\n            <td><%= migration[:status] %></td>\n          </tr>\n          <tr>\n            <th>Migration ID</th>\n            <td><%= migration[:version] %></td>\n          </tr>\n          <tr>\n            <th>Branch</th>\n            <td><%= migration[:branch] %></td>\n          </tr>\n          <tr>\n            <th>Database</th>\n            <td><%= migration[:database] %></td>\n          </tr>\n          <tr>\n            <th>Path</th>\n            <td>\n              <%= migration[:filename] %>\n              <% source = migration[:source].to_s %>\n              <% if source.present? %>\n                <span class=\"source-badge\"><%= source.upcase %></span>\n              <% end %>\n            </td>\n          </tr>\n        </tbody>\n      </table>\n\n      <h3>Migration Code</h3>\n      <div>\n        <pre><%= File.read(migration[:filename]) %></pre>\n      </div>\n      <div class='button-container'>\n        <%= link_to '← Back', migrations_path, class: 'button' %>\n        <%= button_to '⎌ Rollback',\n                      rollback_migration_path(id: migration[:version], database: migration[:database]),\n                      method: :post,\n                      class: 'button migration-action',\n                      style: ('display: none;' if migration[:status] == \"down\") %>\n        <%= button_to '⬆ Migrate',\n                      migrate_migration_path(id: migration[:version], database: migration[:database]),\n                      method: :post,\n                      class: 'button migration-action',\n                      style: ('display: none;' if migration[:status] == \"up\" || migration[:phantom]) %>\n      </div>\n    </div>\n  </body>\n</html>\n"
  },
  {
    "path": "app/views/actual_db_schema/phantom_migrations/index.html.erb",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>Phantom Migrations</title>\n    <%= render partial: 'actual_db_schema/shared/js' %>\n    <%= render partial: 'actual_db_schema/shared/style' %>\n  </head>\n  <body>\n    <div>\n      <% flash.each do |key, message| %>\n        <div class=\"flash <%= key %>\"><%= message %></div>\n      <% end %>\n      <h2>Phantom Migrations</h2>\n      <div class=\"top-buttons\">\n        <%= link_to 'All Migrations', migrations_path, class: \"top-button\" %>\n        <% if phantom_migrations.present? %>\n          <%= button_to '⎌ Rollback all',\n                        rollback_all_phantom_migrations_path,\n                        method: :post,\n                        class: 'button migration-action' %>\n        <% end %>\n      </div>\n      <% if phantom_migrations.present? %>\n        <table>\n          <thead>\n            <tr>\n              <th>Status</th>\n              <th>Migration ID</th>\n              <th>Name</th>\n              <th>Branch</th>\n              <th>Database</th>\n              <th>Actions</th>\n            </tr>\n          </thead>\n          <tbody>\n            <% phantom_migrations.each do |migration| %>\n              <tr class=\"migration-row phantom\">\n                <td><%= migration[:status] %></td>\n                <td><%= migration[:version] %></td>\n                <td>\n                  <div class=\"truncate-text\" title=\"<%= migration[:name] %>\">\n                    <%= migration[:name] %>\n                  </div>\n                </td>\n                <td><%= migration[:branch] %></td>\n                <td><%= migration[:database] %></td>\n                <td>\n                  <div class='button-container'>\n                    <%= link_to '👁 Show',\n                                phantom_migration_path(id: migration[:version], database: migration[:database]),\n                                class: 'button' %>\n                    <%= button_to '⎌ Rollback',\n                                  rollback_phantom_migration_path(id: migration[:version], database: migration[:database]),\n                                  method: :post,\n                                  class: 'button migration-action' %>\n                  </div>\n                </td>\n              </tr>\n            <% end %>\n          </tbody>\n        </table>\n      <% else %>\n        <p>No phantom migrations found.</p>\n      <% end %>\n    </div>\n  </body>\n</html>\n"
  },
  {
    "path": "app/views/actual_db_schema/phantom_migrations/show.html.erb",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>Phantom Migration Details</title>\n    <%= render partial: 'actual_db_schema/shared/js' %>\n    <%= render partial: 'actual_db_schema/shared/style' %>\n  </head>\n  <body>\n    <div>\n      <% flash.each do |key, message| %>\n        <div class=\"flash <%= key %>\"><%= message %></div>\n      <% end %>\n      <h2>Phantom Migration <%= phantom_migration[:name] %> Details</h2>\n      <table>\n        <tbody>\n          <tr>\n            <th>Status</th>\n            <td><%= phantom_migration[:status] %></td>\n          </tr>\n          <tr>\n            <th>Migration ID</th>\n            <td><%= phantom_migration[:version] %></td>\n          </tr>\n          <tr>\n            <th>Branch</th>\n            <td><%= phantom_migration[:branch] %></td>\n          </tr>\n          <tr>\n            <th>Database</th>\n            <td><%= phantom_migration[:database] %></td>\n          </tr>\n          <tr>\n            <th>Path</th>\n            <td>\n              <%= phantom_migration[:filename] %>\n              <% source = phantom_migration[:source].to_s %>\n              <% if source.present? %>\n                <span class=\"source-badge\"><%= source.upcase %></span>\n              <% end %>\n            </td>\n          </tr>\n        </tbody>\n      </table>\n\n      <h3>Migration Code</h3>\n      <div>\n        <pre><%= File.read(phantom_migration[:filename]) %></pre>\n      </div>\n      <div class='button-container'>\n        <%= link_to '← Back', phantom_migrations_path, class: 'button' %>\n        <%= button_to '⎌ Rollback',\n                      rollback_phantom_migration_path(id: params[:id], database: params[:database]),\n                      method: :post,\n                      class: 'button migration-action' %>\n      </div>\n    </div>\n  </body>\n</html>\n"
  },
  {
    "path": "app/views/actual_db_schema/schema/index.html.erb",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>Database Schema</title>\n    <%= render partial: 'actual_db_schema/shared/js' %>\n    <%= render partial: 'actual_db_schema/shared/style' %>\n  </head>\n  <body>\n    <div>\n      <% flash.each do |key, message| %>\n        <div class=\"flash <%= key %>\"><%= message %></div>\n      <% end %>\n      <h2>Database Schema</h2>\n      <div class=\"top-controls\">\n        <div class=\"top-buttons\">\n          <%= link_to 'All Migrations', migrations_path, class: \"top-button\" %>\n        </div>\n        <div class=\"top-search\">\n          <%= form_tag schema_path, method: :get, class: \"search-form\" do %>\n            <span class=\"search-icon\">🔍</span>\n            <%= text_field_tag :table, params[:table], placeholder: \"Filter by table name\", class: \"search-input\" %>\n          <% end %>\n        </div>\n      </div>\n\n      <div class=\"schema-diff\">\n        <pre><%= raw schema_diff_html %></pre>\n      </div>\n    </div>\n  </body>\n</html>\n"
  },
  {
    "path": "app/views/actual_db_schema/shared/_js.html",
    "content": "<script>\n  document.addEventListener('DOMContentLoaded', function() {\n    const migrationActions = document.querySelectorAll('.migration-action');\n\n    migrationActions.forEach(button => {\n      button.addEventListener('click', function(event) {\n        const confirmMessage = button.dataset.confirm;\n          if (confirmMessage && !confirm(confirmMessage)) {\n            event.preventDefault();\n            return;\n        }\n\n        const originalText = button.value;\n        button.value = 'Loading...';\n        disableButtons();\n\n        fetch(event.target.form.action, {\n            method: 'POST'\n          })\n          .then(response => {\n            if (response.ok) {\n              window.location.reload();\n            } else {\n              throw new Error('Network response was not ok.');\n            }\n          })\n          .catch(error => {\n            console.error('There has been a problem with your fetch operation:', error);\n            enableButtons();\n            button.value = originalText;\n          });\n\n        event.preventDefault();\n      });\n    });\n\n    function disableButtons() {\n      migrationActions.forEach(button => {\n        button.disabled = true;\n      });\n    }\n\n    function enableButtons() {\n      migrationActions.forEach(button => {\n        button.disabled = false;\n      });\n    }\n  });\n</script>\n"
  },
  {
    "path": "app/views/actual_db_schema/shared/_style.html",
    "content": "<style>\n  body {\n    margin: 8px;\n    background-color: #fff;\n    color: #333;\n  }\n\n  body, p, td {\n    font-family: helvetica, verdana, arial, sans-serif;\n    font-size: 13px;\n    line-height: 18px;\n  }\n\n  h2 {\n    padding-left: 10px;\n  }\n\n  p {\n    padding-left: 10px;\n  }\n\n  table {\n    margin: 0;\n    border-collapse: collapse;\n\n    thead tr {\n      border-bottom: 2px solid #ddd;\n    }\n\n    tbody {\n      .migration-row.phantom {\n        background-color: #fff3f3;\n      }\n\n      .migration-row.normal {\n        background-color: #ffffff;\n      }\n\n      .migration-row:nth-child(odd).phantom {\n        background-color: #ffe6e6;\n      }\n\n      .migration-row:nth-child(odd).normal {\n        background-color: #f9f9f9;\n      }\n    }\n\n    td {\n      padding: 14px 30px;\n    }\n  }\n\n  .top-buttons {\n    margin: 8px;\n    display: flex;\n    align-items: center;\n\n    .top-button {\n      background-color: #ddd;\n    }\n  }\n\n  .button, .top-button {\n    font-weight: bold;\n    color: #000;\n    border: none;\n    padding: 5px 10px;\n    text-align: center;\n    text-decoration: none;\n    display: inline-block;\n    margin: 0 2px;\n    margin-right: 8px;\n    cursor: pointer;\n    border-radius: 4px;\n    transition: background-color 0.3s;\n    background: none;\n  }\n\n  .button:hover, .top-button:hover {\n    color: #fff;\n    background-color: #000;\n  }\n\n  .button:disabled, .button:hover:disabled {\n    background-color: transparent;\n    color: #666;\n    cursor: not-allowed;\n  }\n\n  .button-container {\n    display: flex;\n  }\n\n  pre {\n    background-color: #f7f7f7;\n    padding: 10px;\n    border: 1px solid #ddd;\n  }\n\n  .truncate-text {\n    max-width: 200px;\n    overflow: hidden;\n    text-overflow: ellipsis;\n  }\n\n  .flash {\n    padding: 10px;\n    margin-bottom: 10px;\n    border-radius: 5px;\n  }\n\n  .flash.notice {\n    background-color: #d4edda;\n    color: #155724;\n  }\n\n  .flash.alert {\n    background-color: #f8d7da;\n    color: #721c24;\n  }\n\n  .container {\n    display: inline-block;\n    max-width: 100%;\n  }\n\n  .top-controls {\n    display: flex;\n    justify-content: space-between; \n    align-items: center;\n    width: 100%;\n  }\n\n  .top-search {\n    display: flex;\n    align-items: center;\n    justify-content: flex-end;\n  }\n\n  .search-form {\n    display: flex;\n    align-items: center;\n  }\n\n  .search-form .search-icon {\n    margin-right: 5px;\n    font-size: 16px;\n  }\n\n  .search-form .search-input {\n    padding: 5px;\n    border: 1px solid #ccc;\n    border-radius: 4px;\n    font-size: 13px;\n    width: 250px;\n  }\n\n  .schema-diff {\n    margin-left: 8px;\n  }\n\n  .source-badge {\n    display: inline-block;\n    margin-left: 8px;\n    padding: 2px 6px;\n    border-radius: 10px;\n    font-size: 11px;\n    font-weight: bold;\n    letter-spacing: 0.3px;\n    background-color: #e8f1ff;\n    color: #1d4ed8;\n  }\n</style>\n"
  },
  {
    "path": "bin/console",
    "content": "#!/usr/bin/env ruby\n# frozen_string_literal: true\n\nrequire \"bundler/setup\"\nrequire \"actual_db_schema\"\n\n# You can add fixtures and/or initialization code here to make experimenting\n# with your gem easier. You can also use a different console, if you like.\n\n# (If you use this, don't forget to add pry to your Gemfile!)\n# require \"pry\"\n# Pry.start\n\nrequire \"irb\"\nIRB.start(__FILE__)\n"
  },
  {
    "path": "bin/setup",
    "content": "#!/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 to do here\n"
  },
  {
    "path": "config/routes.rb",
    "content": "# frozen_string_literal: true\n\nActualDbSchema::Engine.routes.draw do\n  resources :migrations, only: %i[index show] do\n    member do\n      post :rollback\n      post :migrate\n    end\n  end\n  resources :phantom_migrations, only: %i[index show] do\n    member do\n      post :rollback\n    end\n    collection do\n      post :rollback_all\n    end\n  end\n  resources :broken_versions, only: %i[index] do\n    member do\n      post :delete\n    end\n    collection do\n      post :delete_all\n    end\n  end\n\n  get \"schema\", to: \"schema#index\", as: :schema\nend\n"
  },
  {
    "path": "docker/mysql-init/create_secondary_db.sql",
    "content": "CREATE DATABASE actual_db_schema_test_secondary;\n"
  },
  {
    "path": "docker/postgres-init/create_secondary_db.sql",
    "content": "CREATE DATABASE actual_db_schema_test_secondary;\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: '3.8'\n\nservices:\n  postgres:\n    image: postgres:14\n    environment:\n      POSTGRES_USER: postgres\n      POSTGRES_PASSWORD: password\n      POSTGRES_DB: actual_db_schema_test\n    ports:\n      - \"5432:5432\"\n    volumes:\n      - ./docker/postgres-init:/docker-entrypoint-initdb.d\n\n  mysql:\n    image: mysql:8.0\n    environment:\n      MYSQL_ROOT_PASSWORD: password\n      MYSQL_DATABASE: actual_db_schema_test\n    ports:\n      - \"3306:3306\"\n    volumes:\n    - ./docker/mysql-init:/docker-entrypoint-initdb.d\n"
  },
  {
    "path": "gemfiles/rails.6.0.gemfile",
    "content": "# frozen_string_literal: true\n\n# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"activerecord\", \"~> 6.0.0\"\ngem \"activesupport\", \"~> 6.0.0\"\ngem \"minitest\", \"~> 5.0\"\ngem \"mysql2\", \"~> 0.5.2\"\ngem \"pg\", \"~> 1.5\"\ngem \"rake\"\ngem \"rubocop\", \"~> 1.21\"\ngem \"sqlite3\", \"~> 1.4.0\"\n\ngemspec path: \"../\"\n"
  },
  {
    "path": "gemfiles/rails.6.1.gemfile",
    "content": "# frozen_string_literal: true\n\n# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"activerecord\", \"~> 6.1.0\"\ngem \"activesupport\", \"~> 6.1.0\"\ngem \"minitest\", \"~> 5.0\"\ngem \"mysql2\", \"~> 0.5.2\"\ngem \"pg\", \"~> 1.5\"\ngem \"rake\"\ngem \"rubocop\", \"~> 1.21\"\ngem \"sqlite3\", \"~> 1.4.0\"\n\ngemspec path: \"../\"\n"
  },
  {
    "path": "gemfiles/rails.7.0.gemfile",
    "content": "# frozen_string_literal: true\n\n# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"activerecord\", \"~> 7.0.0\"\ngem \"activesupport\", \"~> 7.0.0\"\ngem \"minitest\", \"~> 5.0\"\ngem \"mysql2\", \"~> 0.5.2\"\ngem \"pg\", \"~> 1.5\"\ngem \"rake\"\ngem \"rubocop\", \"~> 1.21\"\ngem \"sqlite3\", \"~> 1.4.0\"\n\ngemspec path: \"../\"\n"
  },
  {
    "path": "gemfiles/rails.7.1.gemfile",
    "content": "# frozen_string_literal: true\n\n# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"activerecord\", \"~> 7.1.0\"\ngem \"activesupport\", \"~> 7.1.0\"\ngem \"minitest\", \"~> 5.0\"\ngem \"mysql2\", \"~> 0.5.2\"\ngem \"pg\", \"~> 1.5\"\ngem \"rake\"\ngem \"rubocop\", \"~> 1.21\"\ngem \"sqlite3\", \"~> 1.4.0\"\n\ngemspec path: \"../\"\n"
  },
  {
    "path": "gemfiles/rails.edge.gemfile",
    "content": "# frozen_string_literal: true\n\n# This file was generated by Appraisal\n\nsource \"https://rubygems.org\"\n\ngem \"activerecord\", \">= 7.2.0.beta\"\ngem \"activesupport\", \">= 7.2.0.beta\"\ngem \"minitest\", \"~> 5.0\"\ngem \"mysql2\", \"~> 0.5.2\"\ngem \"pg\", \"~> 1.5\"\ngem \"rake\"\ngem \"rubocop\", \"~> 1.21\"\ngem \"rails\", \">= 7.2.0.beta\"\ngem \"sqlite3\"\n\ngemspec path: \"../\"\n"
  },
  {
    "path": "lib/actual_db_schema/commands/base.rb",
    "content": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n  module Commands\n    # Base class for all commands\n    class Base\n      attr_reader :context\n\n      def initialize(context)\n        @context = context\n      end\n\n      def call\n        unless ActualDbSchema.config.fetch(:enabled, true)\n          raise \"ActualDbSchema is disabled. Set ActualDbSchema.config[:enabled] = true to enable it.\"\n        end\n\n        call_impl\n      end\n\n      private\n\n      def call_impl\n        raise NotImplementedError\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/actual_db_schema/commands/list.rb",
    "content": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n  module Commands\n    # Shows the list of phantom migrations\n    class List < Base\n      private\n\n      def call_impl\n        preambule\n        table\n      end\n\n      def indexed_phantom_migrations\n        @indexed_phantom_migrations ||= context.phantom_migrations.index_by { |m| m.version.to_s }\n      end\n\n      def preambule\n        puts \"\\nPhantom migrations\\n\\n\"\n        puts \"Below is a list of irrelevant migrations executed in unmerged branches.\"\n        puts \"To bring your database schema up to date, the migrations marked as \\\"up\\\" should be rolled back.\"\n        puts \"\\ndatabase: #{ActualDbSchema.db_config[:database]}\\n\\n\"\n        puts header.join(\"  \")\n        puts \"-\" * separator_width\n      end\n\n      def separator_width\n        header.map(&:length).sum + (header.size - 1) * 2\n      end\n\n      def header\n        @header ||=\n          [\n            \"Status\".center(8),\n            \"Migration ID\".ljust(14),\n            \"Branch\".ljust(branch_column_width),\n            \"Migration File\".ljust(16)\n          ]\n      end\n\n      def table\n        context.migrations_status.each do |status, version|\n          line = line_for(status, version)\n          puts line if line\n        end\n      end\n\n      def line_for(status, version)\n        migration = indexed_phantom_migrations[version]\n        return unless migration\n\n        [\n          status.center(8),\n          version.to_s.ljust(14),\n          branch_for(version).ljust(branch_column_width),\n          migration.filename.gsub(\"#{Rails.root}/\", \"\")\n        ].join(\"  \")\n      end\n\n      def metadata\n        @metadata ||= ActualDbSchema::Store.instance.read\n      end\n\n      def branch_for(version)\n        metadata.fetch(version, {})[:branch] || \"unknown\"\n      end\n\n      def longest_branch_name\n        @longest_branch_name ||=\n          metadata.values.map { |v| v[:branch] }.compact.max_by(&:length) || \"unknown\"\n      end\n\n      def branch_column_width\n        longest_branch_name.length\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/actual_db_schema/commands/rollback.rb",
    "content": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n  module Commands\n    # Rolls back all phantom migrations\n    class Rollback < Base\n      include ActualDbSchema::OutputFormatter\n      include ActionView::Helpers::TextHelper\n\n      def initialize(context, manual_mode: false)\n        @manual_mode = manual_mode || manual_mode_default?\n        super(context)\n      end\n\n      private\n\n      def call_impl\n        rolled_back = context.rollback_branches(manual_mode: @manual_mode)\n\n        return unless rolled_back || ActualDbSchema.failed.any?\n\n        ActualDbSchema.failed.empty? ? print_success : print_error\n      end\n\n      def print_success\n        puts colorize(\"[ActualDbSchema] All phantom migrations rolled back successfully! 🎉\", :green)\n      end\n\n      def print_error\n        header_message = <<~HEADER\n          #{ActualDbSchema.failed.count} phantom migration(s) could not be rolled back automatically.\n\n          Try these steps to fix and move forward:\n            1. Ensure the migrations are reversible (define #up and #down methods or use #reversible).\n            2. If the migration references code or tables from another branch, restore or remove them.\n            3. Once fixed, run `rails db:migrate` again.\n\n          Below are the details of the problematic migrations:\n        HEADER\n\n        print_error_summary(\"#{header_message}\\n#{failed_migrations_list}\")\n      end\n\n      def failed_migrations_list\n        ActualDbSchema.failed.map.with_index(1) do |failed, index|\n          migration_details = colorize(\"Migration ##{index}:\\n\", :yellow)\n          migration_details += \"  File: #{failed.short_filename}\\n\"\n          migration_details += \"  Schema: #{failed.schema}\\n\" if failed.schema\n          migration_details + \"  Branch: #{failed.branch}\\n\"\n        end.join(\"\\n\")\n      end\n\n      def print_error_summary(content)\n        width = 100\n        indent = 4\n        gem_name = \"ActualDbSchema\"\n\n        puts colorize(\"╔═ [#{gem_name}] #{\"═\" * (width - gem_name.length - 5)}╗\", :red)\n        print_wrapped_content(content, width, indent)\n        puts colorize(\"╚#{\"═\" * width}╝\", :red)\n      end\n\n      def print_wrapped_content(content, width, indent)\n        usable_width = width - indent - 4\n        wrapped_content = word_wrap(content, line_width: usable_width)\n        wrapped_content.each_line do |line|\n          puts \"#{\" \" * indent}#{line.chomp}\"\n        end\n      end\n\n      def manual_mode_default?\n        ActualDbSchema.config[:auto_rollback_disabled]\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/actual_db_schema/configuration.rb",
    "content": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n  # Manages the configuration settings for the gem.\n  class Configuration\n    attr_accessor :enabled, :auto_rollback_disabled, :ui_enabled, :git_hooks_enabled, :multi_tenant_schemas,\n                  :console_migrations_enabled, :migrated_folder, :migrations_storage, :excluded_databases\n\n    def initialize\n      apply_defaults(default_settings)\n    end\n\n    def [](key)\n      public_send(key)\n    end\n\n    def []=(key, value)\n      public_send(\"#{key}=\", value)\n      return unless key.to_sym == :migrations_storage && defined?(ActualDbSchema::Store)\n\n      ActualDbSchema::Store.instance.reset_adapter\n    end\n\n    def fetch(key, default = nil)\n      if respond_to?(key)\n        public_send(key)\n      else\n        default\n      end\n    end\n\n    private\n\n    def default_settings\n      {\n        enabled: enabled_by_default?,\n        auto_rollback_disabled: env_enabled?(\"ACTUAL_DB_SCHEMA_AUTO_ROLLBACK_DISABLED\"),\n        ui_enabled: ui_enabled_by_default?,\n        git_hooks_enabled: env_enabled?(\"ACTUAL_DB_SCHEMA_GIT_HOOKS_ENABLED\"),\n        multi_tenant_schemas: nil,\n        console_migrations_enabled: env_enabled?(\"ACTUAL_DB_SCHEMA_CONSOLE_MIGRATIONS_ENABLED\"),\n        migrated_folder: ENV[\"ACTUAL_DB_SCHEMA_MIGRATED_FOLDER\"].present?,\n        migrations_storage: migrations_storage_from_env,\n        excluded_databases: parse_excluded_databases_env\n      }\n    end\n\n    def enabled_by_default?\n      Rails.env.development?\n    end\n\n    def ui_enabled_by_default?\n      Rails.env.development? || env_enabled?(\"ACTUAL_DB_SCHEMA_UI_ENABLED\")\n    end\n\n    def env_enabled?(key)\n      ENV[key].present?\n    end\n\n    def migrations_storage_from_env\n      ENV.fetch(\"ACTUAL_DB_SCHEMA_MIGRATIONS_STORAGE\", \"file\").to_sym\n    end\n\n    def parse_excluded_databases_env\n      return [] unless ENV[\"ACTUAL_DB_SCHEMA_EXCLUDED_DATABASES\"].present?\n\n      ENV[\"ACTUAL_DB_SCHEMA_EXCLUDED_DATABASES\"]\n        .split(\",\")\n        .map(&:strip)\n        .reject(&:empty?)\n        .map(&:to_sym)\n    end\n\n    def apply_defaults(settings)\n      settings.each do |key, value|\n        instance_variable_set(\"@#{key}\", value)\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/actual_db_schema/console_migrations.rb",
    "content": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n  # Provides methods for executing schema modification commands directly in the Rails console.\n  module ConsoleMigrations\n    extend self\n\n    SCHEMA_METHODS = %i[\n      create_table\n      create_join_table\n      drop_table\n      change_table\n      add_column\n      remove_column\n      change_column\n      change_column_null\n      change_column_default\n      rename_column\n      add_index\n      remove_index\n      rename_index\n      add_timestamps\n      remove_timestamps\n      reversible\n      add_reference\n      remove_reference\n      add_foreign_key\n      remove_foreign_key\n    ].freeze\n\n    SCHEMA_METHODS.each do |method_name|\n      define_method(method_name) do |*args, **kwargs, &block|\n        if kwargs.any?\n          migration_instance.public_send(method_name, *args, **kwargs, &block)\n        else\n          migration_instance.public_send(method_name, *args, &block)\n        end\n      end\n    end\n\n    private\n\n    def migration_instance\n      @migration_instance ||= Class.new(ActiveRecord::Migration[ActiveRecord::Migration.current_version]) {}.new\n    end\n  end\nend\n"
  },
  {
    "path": "lib/actual_db_schema/engine.rb",
    "content": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n  # It isolates the namespace to avoid conflicts with the main application.\n  class Engine < ::Rails::Engine\n    isolate_namespace ActualDbSchema\n\n    initializer \"actual_db_schema.initialize\" do |app|\n      if ActualDbSchema.config[:ui_enabled]\n        app.routes.append do\n          mount ActualDbSchema::Engine => \"/rails\"\n        end\n      end\n    end\n\n    initializer \"actual_db_schema.schema_dump_exclusions\" do\n      ActiveSupport.on_load(:active_record) do\n        ActualDbSchema::Engine.apply_schema_dump_exclusions\n      end\n    end\n\n    def self.apply_schema_dump_exclusions\n      ignore_schema_dump_table(ActualDbSchema::Store::DbAdapter::TABLE_NAME)\n      ignore_schema_dump_table(ActualDbSchema::RollbackStatsRepository::TABLE_NAME)\n      return unless schema_dump_flags_supported?\n      return unless schema_dump_connection_available?\n\n      apply_structure_dump_flags(ActualDbSchema::Store::DbAdapter::TABLE_NAME)\n      apply_structure_dump_flags(ActualDbSchema::RollbackStatsRepository::TABLE_NAME)\n    end\n\n    class << self\n      private\n\n      def ignore_schema_dump_table(table_name)\n        return unless defined?(ActiveRecord::SchemaDumper)\n        return unless ActiveRecord::SchemaDumper.respond_to?(:ignore_tables)\n\n        ActiveRecord::SchemaDumper.ignore_tables |= [table_name]\n      end\n\n      def schema_dump_flags_supported?\n        defined?(ActiveRecord::Tasks::DatabaseTasks) &&\n          ActiveRecord::Tasks::DatabaseTasks.respond_to?(:structure_dump_flags)\n      end\n\n      # Avoid touching db config unless we explicitly use DB storage\n      # or a connection is already available.\n      def schema_dump_connection_available?\n        has_connection = begin\n          ActiveRecord::Base.connection_pool.connected?\n        rescue ActiveRecord::ConnectionNotDefined, ActiveRecord::ConnectionNotEstablished\n          false\n        end\n\n        ActualDbSchema.config[:migrations_storage] == :db || has_connection\n      end\n\n      def apply_structure_dump_flags(table_name)\n        flags = Array(ActiveRecord::Tasks::DatabaseTasks.structure_dump_flags)\n        adapter = ActualDbSchema.db_config[:adapter].to_s\n        database = database_name\n\n        if adapter.match?(/postgres/i)\n          flag = \"--exclude-table=#{table_name}*\"\n          flags << flag unless flags.include?(flag)\n        elsif adapter.match?(/mysql/i) && database\n          flag = \"--ignore-table=#{database}.#{table_name}\"\n          flags << flag unless flags.include?(flag)\n        end\n\n        ActiveRecord::Tasks::DatabaseTasks.structure_dump_flags = flags\n      end\n\n      def database_name\n        database = ActualDbSchema.db_config[:database]\n        if database.nil? && ActiveRecord::Base.respond_to?(:connection_db_config)\n          database = ActiveRecord::Base.connection_db_config&.database\n        end\n        database\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/actual_db_schema/failed_migration.rb",
    "content": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n  FailedMigration = Struct.new(:migration, :exception, :branch, :schema, keyword_init: true) do\n    def filename\n      migration.filename\n    end\n\n    def short_filename\n      migration.filename.sub(File.join(Rails.root, \"/\"), \"\")\n    end\n  end\nend\n"
  },
  {
    "path": "lib/actual_db_schema/git.rb",
    "content": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n  # Git helper\n  class Git\n    def self.current_branch\n      branch = `git rev-parse --abbrev-ref HEAD 2>/dev/null`.strip\n      branch.empty? ? \"unknown\" : branch\n    rescue Errno::ENOENT\n      \"unknown\"\n    end\n  end\nend\n"
  },
  {
    "path": "lib/actual_db_schema/git_hooks.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"fileutils\"\n\nmodule ActualDbSchema\n  # Handles the installation of a git post-checkout hook that rolls back phantom migrations when switching branches\n  class GitHooks\n    include ActualDbSchema::OutputFormatter\n\n    POST_CHECKOUT_MARKER_START = \"# >>> BEGIN ACTUAL_DB_SCHEMA\"\n    POST_CHECKOUT_MARKER_END   = \"# <<< END ACTUAL_DB_SCHEMA\"\n\n    POST_CHECKOUT_HOOK_ROLLBACK = <<~BASH\n      #{POST_CHECKOUT_MARKER_START}\n      # ActualDbSchema post-checkout hook (ROLLBACK)\n      # Runs db:rollback_branches on branch checkout.\n\n      # Check if this is a file checkout or creating a new branch\n      if [ \"$3\" == \"0\" ] || [ \"$1\" == \"$2\" ]; then\n        exit 0\n      fi\n\n      if [ -f ./bin/rails ]; then\n        if [ -n \"$ACTUAL_DB_SCHEMA_GIT_HOOKS_ENABLED\" ]; then\n          GIT_HOOKS_ENABLED=\"$ACTUAL_DB_SCHEMA_GIT_HOOKS_ENABLED\"\n        else\n          GIT_HOOKS_ENABLED=$(./bin/rails runner \"puts ActualDbSchema.config[:git_hooks_enabled]\" 2>/dev/null)\n        fi\n\n        if [ \"$GIT_HOOKS_ENABLED\" == \"true\" ]; then\n          ./bin/rails db:rollback_branches\n        fi\n      fi\n      #{POST_CHECKOUT_MARKER_END}\n    BASH\n\n    POST_CHECKOUT_HOOK_MIGRATE = <<~BASH\n      #{POST_CHECKOUT_MARKER_START}\n      # ActualDbSchema post-checkout hook (MIGRATE)\n      # Runs db:migrate on branch checkout.\n\n      # Check if this is a file checkout or creating a new branch\n      if [ \"$3\" == \"0\" ] || [ \"$1\" == \"$2\" ]; then\n        exit 0\n      fi\n\n      if [ -f ./bin/rails ]; then\n        if [ -n \"$ACTUAL_DB_SCHEMA_GIT_HOOKS_ENABLED\" ]; then\n          GIT_HOOKS_ENABLED=\"$ACTUAL_DB_SCHEMA_GIT_HOOKS_ENABLED\"\n        else\n          GIT_HOOKS_ENABLED=$(./bin/rails runner \"puts ActualDbSchema.config[:git_hooks_enabled]\" 2>/dev/null)\n        fi\n\n        if [ \"$GIT_HOOKS_ENABLED\" == \"true\" ]; then\n          ./bin/rails db:migrate\n        fi\n      fi\n      #{POST_CHECKOUT_MARKER_END}\n    BASH\n\n    def initialize(strategy: :rollback)\n      @strategy = strategy\n    end\n\n    def install_post_checkout_hook\n      return unless hooks_directory_present?\n\n      if File.exist?(hook_path)\n        handle_existing_hook\n      else\n        create_new_hook\n      end\n    end\n\n    private\n\n    def hook_code\n      @strategy == :migrate ? POST_CHECKOUT_HOOK_MIGRATE : POST_CHECKOUT_HOOK_ROLLBACK\n    end\n\n    def hooks_dir\n      @hooks_dir ||= Rails.root.join(\".git\", \"hooks\")\n    end\n\n    def hook_path\n      @hook_path ||= hooks_dir.join(\"post-checkout\")\n    end\n\n    def hooks_directory_present?\n      return true if Dir.exist?(hooks_dir)\n\n      puts colorize(\"[ActualDbSchema] .git/hooks directory not found. Please ensure this is a Git repository.\", :gray)\n    end\n\n    def handle_existing_hook\n      return update_hook if markers_exist?\n      return install_hook if safe_install?\n\n      show_manual_install_instructions\n    end\n\n    def create_new_hook\n      contents = <<~BASH\n        #!/usr/bin/env bash\n\n        #{hook_code}\n      BASH\n\n      write_hook_file(contents)\n      print_success\n    end\n\n    def markers_exist?\n      contents = File.read(hook_path)\n      contents.include?(POST_CHECKOUT_MARKER_START) && contents.include?(POST_CHECKOUT_MARKER_END)\n    end\n\n    def update_hook\n      contents = File.read(hook_path)\n      new_contents = replace_marker_contents(contents)\n\n      if new_contents == contents\n        message = \"[ActualDbSchema] post-checkout git hook already contains the necessary code. Nothing to update.\"\n        puts colorize(message, :gray)\n      else\n        write_hook_file(new_contents)\n        puts colorize(\"[ActualDbSchema] post-checkout git hook updated successfully at #{hook_path}\", :green)\n      end\n    end\n\n    def replace_marker_contents(contents)\n      contents.gsub(\n        /#{Regexp.quote(POST_CHECKOUT_MARKER_START)}.*#{Regexp.quote(POST_CHECKOUT_MARKER_END)}/m,\n        hook_code.strip\n      )\n    end\n\n    def safe_install?\n      puts colorize(\"[ActualDbSchema] A post-checkout hook already exists at #{hook_path}.\", :gray)\n      puts \"Overwrite the existing hook at #{hook_path}? [y,n] \"\n\n      answer = $stdin.gets.chomp.downcase\n      answer.start_with?(\"y\")\n    end\n\n    def install_hook\n      contents = File.read(hook_path)\n      new_contents = <<~BASH\n        #{contents.rstrip}\n\n        #{hook_code}\n      BASH\n\n      write_hook_file(new_contents)\n      print_success\n    end\n\n    def show_manual_install_instructions\n      puts colorize(\"[ActualDbSchema] You can follow these steps to manually install the hook:\", :yellow)\n      puts <<~MSG\n\n        1. Open the existing post-checkout hook at:\n           #{hook_path}\n\n        2. Insert the following lines into that file (preferably at the end or in a relevant section).\n           Make sure you include the #{POST_CHECKOUT_MARKER_START} and #{POST_CHECKOUT_MARKER_END} lines:\n\n        #{hook_code}\n\n        3. Ensure the post-checkout file is executable:\n           chmod +x #{hook_path}\n\n        4. Done! Now when you switch branches, phantom migrations will be rolled back automatically (if enabled).\n\n      MSG\n    end\n\n    def write_hook_file(contents)\n      File.open(hook_path, \"w\") { |file| file.write(contents) }\n      FileUtils.chmod(\"+x\", hook_path)\n    end\n\n    def print_success\n      puts colorize(\"[ActualDbSchema] post-checkout git hook installed successfully at #{hook_path}\", :green)\n    end\n  end\nend\n"
  },
  {
    "path": "lib/actual_db_schema/instrumentation.rb",
    "content": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n  module Instrumentation\n    ROLLBACK_EVENT = \"rollback.actual_db_schema\"\n  end\nend\n"
  },
  {
    "path": "lib/actual_db_schema/migration.rb",
    "content": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n  # The Migration class is responsible for managing and retrieving migration information\n  class Migration\n    include Singleton\n\n    Migration = Struct.new(:status, :version, :name, :branch, :database, :filename, :phantom, :source,\n                           keyword_init: true)\n\n    def all_phantom\n      migrations = []\n\n      MigrationContext.instance.each do |context|\n        indexed_migrations = context.phantom_migrations.index_by { |m| m.version.to_s }\n\n        context.migrations_status.each do |status, version|\n          migration = indexed_migrations[version]\n          migrations << build_migration_struct(status, migration) if should_include?(status, migration)\n        end\n      end\n\n      sort_migrations_desc(migrations)\n    end\n\n    def all\n      migrations = []\n\n      MigrationContext.instance.each do |context|\n        indexed_migrations = context.migrations.index_by { |m| m.version.to_s }\n\n        context.migrations_status.each do |status, version|\n          migration = indexed_migrations[version]\n          migrations << build_migration_struct(status, migration) if should_include?(status, migration)\n        end\n      end\n\n      sort_migrations_desc(migrations)\n    end\n\n    def find(version, database)\n      MigrationContext.instance.each do |context|\n        next unless ActualDbSchema.db_config[:database] == database\n\n        migration = find_migration_in_context(context, version)\n        return migration if migration\n      end\n      nil\n    end\n\n    def rollback(version, database)\n      MigrationContext.instance.each do |context|\n        next unless ActualDbSchema.db_config[:database] == database\n\n        if context.migrations.detect { |m| m.version.to_s == version }\n          context.run(:down, version.to_i)\n          break\n        end\n      end\n    end\n\n    def rollback_all\n      MigrationContext.instance.each(&:rollback_branches)\n    end\n\n    def migrate(version, database)\n      MigrationContext.instance.each do |context|\n        next unless ActualDbSchema.db_config[:database] == database\n\n        if context.migrations.detect { |m| m.version.to_s == version }\n          context.run(:up, version.to_i)\n          break\n        end\n      end\n    end\n\n    def broken_versions\n      broken = []\n      MigrationContext.instance.each do |context|\n        context.migrations_status.each do |status, version, name|\n          next unless name == \"********** NO FILE **********\"\n\n          broken << Migration.new(\n            status: status,\n            version: version.to_s,\n            name: name,\n            branch: branch_for(version),\n            database: ActualDbSchema.db_config[:database]\n          )\n        end\n      end\n\n      broken\n    end\n\n    def delete(version, database)\n      validate_broken_migration(version, database)\n\n      MigrationContext.instance.each do\n        next if database && ActualDbSchema.db_config[:database] != database\n        next if ActiveRecord::Base.connection.select_values(\"SELECT version FROM schema_migrations\").exclude?(version)\n\n        ActiveRecord::Base.connection.execute(\"DELETE FROM schema_migrations WHERE version = '#{version}'\")\n        break\n      end\n    end\n\n    def delete_all\n      broken_versions.each do |version|\n        delete(version.version, version.database)\n      end\n    end\n\n    private\n\n    def build_migration_struct(status, migration)\n      Migration.new(\n        status: status,\n        version: migration.version.to_s,\n        name: migration.name,\n        branch: branch_for(migration.version),\n        database: ActualDbSchema.db_config[:database],\n        filename: migration.filename,\n        phantom: phantom?(migration),\n        source: ActualDbSchema::Store.instance.source_for(migration.version)\n      )\n    end\n\n    def sort_migrations_desc(migrations)\n      migrations.sort_by { |migration| migration[:version].to_i }.reverse if migrations.any?\n    end\n\n    def phantom?(migration)\n      ActualDbSchema::Store.instance.stored_migration?(migration.filename)\n    end\n\n    def should_include?(status, migration)\n      migration && (status == \"up\" || !phantom?(migration))\n    end\n\n    def find_migration_in_context(context, version)\n      migration = context.migrations.detect { |m| m.version.to_s == version }\n      return unless migration\n\n      status = context.migrations_status.detect { |_s, v| v.to_s == version }&.first || \"unknown\"\n      build_migration_struct(status, migration)\n    end\n\n    def branch_for(version)\n      metadata.fetch(version.to_s, {})[:branch] || \"unknown\"\n    end\n\n    def metadata\n      @metadata ||= {}\n      @metadata[ActualDbSchema.db_config[:database]] ||= ActualDbSchema::Store.instance.read\n    end\n\n    def validate_broken_migration(version, database)\n      if database\n        unless broken_versions.any? { |v| v.version == version && v.database == database }\n          raise StandardError, \"Migration is not broken for database #{database}.\"\n        end\n      else\n        raise StandardError, \"Migration is not broken.\" unless broken_versions.any? { |v| v.version == version }\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/actual_db_schema/migration_context.rb",
    "content": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n  # The class manages connections to each database and provides the appropriate migration context for each connection.\n  class MigrationContext\n    include Singleton\n\n    def each\n      original_config = current_config\n      configs.each do |db_config|\n        establish_connection(db_config)\n        yield context\n      end\n    ensure\n      establish_connection(original_config) if original_config\n    end\n\n    private\n\n    def establish_connection(db_config)\n      config = db_config.respond_to?(:config) ? db_config.config : db_config\n      ActiveRecord::Base.establish_connection(config)\n    end\n\n    def current_config\n      if ActiveRecord::Base.respond_to?(:connection_db_config)\n        ActiveRecord::Base.connection_db_config\n      else\n        ActiveRecord::Base.connection_config\n      end\n    end\n\n    def configs\n      all_configs = if ActiveRecord::Base.configurations.is_a?(Hash)\n                      # Rails < 6.0 has a Hash in configurations\n                      [ActiveRecord::Base.configurations[ActiveRecord::Tasks::DatabaseTasks.env]]\n                    else\n                      ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env)\n                    end\n\n      filter_configs(all_configs)\n    end\n\n    def filter_configs(all_configs)\n      all_configs.reject do |db_config|\n        # Skip if database is in the excluded list\n        # Rails 6.0 uses spec_name, Rails 6.1+ uses name\n        db_name = if db_config.respond_to?(:name)\n                    db_config.name.to_sym\n                  elsif db_config.respond_to?(:spec_name)\n                    db_config.spec_name.to_sym\n                  else\n                    :primary\n                  end\n        ActualDbSchema.config.excluded_databases.include?(db_name)\n      end\n    end\n\n    def context\n      ar_version = Gem::Version.new(ActiveRecord::VERSION::STRING)\n      context = if ar_version >= Gem::Version.new(\"7.2.0\") ||\n                   (ar_version >= Gem::Version.new(\"7.1.0\") && ar_version.prerelease?)\n                  ActiveRecord::Base.connection_pool.migration_context\n                else\n                  ActiveRecord::Base.connection.migration_context\n                end\n      context.extend(ActualDbSchema::Patches::MigrationContext)\n    end\n  end\nend\n"
  },
  {
    "path": "lib/actual_db_schema/migration_parser.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"ast\"\nrequire \"prism\"\n\nmodule ActualDbSchema\n  # Parses migration files in a Rails application into a structured hash representation.\n  module MigrationParser\n    extend self\n\n    PARSER_MAPPING = {\n      add_column: ->(args) { parse_add_column(args) },\n      change_column: ->(args) { parse_change_column(args) },\n      remove_column: ->(args) { parse_remove_column(args) },\n      rename_column: ->(args) { parse_rename_column(args) },\n      add_index: ->(args) { parse_add_index(args) },\n      remove_index: ->(args) { parse_remove_index(args) },\n      rename_index: ->(args) { parse_rename_index(args) },\n      create_table: ->(args) { parse_create_table(args) },\n      drop_table: ->(args) { parse_drop_table(args) }\n    }.freeze\n\n    def parse_all_migrations(dirs)\n      changes_by_path = {}\n      handled_files = Set.new\n\n      dirs.each do |dir|\n        Dir[\"#{dir}/*.rb\"].sort.each do |file|\n          base_name = File.basename(file)\n          next if handled_files.include?(base_name)\n\n          changes = parse_file(file).yield_self { |ast| find_migration_changes(ast) }\n          changes_by_path[file] = changes unless changes.empty?\n          handled_files.add(base_name)\n        end\n      end\n\n      changes_by_path\n    end\n\n    private\n\n    def parse_file(file_path)\n      Prism::Translation::Parser.parse_file(file_path)\n    end\n\n    def find_migration_changes(node)\n      return [] unless node.is_a?(Parser::AST::Node)\n\n      changes = []\n      if node.type == :block\n        return process_block_node(node)\n      elsif node.type == :send\n        changes.concat(process_send_node(node))\n      end\n\n      node.children.each { |child| changes.concat(find_migration_changes(child)) if child.is_a?(Parser::AST::Node) }\n\n      changes\n    end\n\n    def process_block_node(node)\n      changes = []\n      send_node = node.children.first\n      return changes unless send_node.type == :send\n\n      method_name = send_node.children[1]\n      return changes unless method_name == :create_table\n\n      change = parse_create_table_with_block(send_node, node)\n      changes << change if change\n      changes\n    end\n\n    def process_send_node(node)\n      changes = []\n      _receiver, method_name, *args = node.children\n      if (parser = PARSER_MAPPING[method_name])\n        change = parser.call(args)\n        changes << change if change\n      end\n\n      changes\n    end\n\n    def parse_add_column(args)\n      return unless args.size >= 3\n\n      {\n        action: :add_column,\n        table: sym_value(args[0]),\n        column: sym_value(args[1]),\n        type: sym_value(args[2]),\n        options: parse_hash(args[3])\n      }\n    end\n\n    def parse_change_column(args)\n      return unless args.size >= 3\n\n      {\n        action: :change_column,\n        table: sym_value(args[0]),\n        column: sym_value(args[1]),\n        type: sym_value(args[2]),\n        options: parse_hash(args[3])\n      }\n    end\n\n    def parse_remove_column(args)\n      return unless args.size >= 2\n\n      {\n        action: :remove_column,\n        table: sym_value(args[0]),\n        column: sym_value(args[1]),\n        options: parse_hash(args[2])\n      }\n    end\n\n    def parse_rename_column(args)\n      return unless args.size >= 3\n\n      {\n        action: :rename_column,\n        table: sym_value(args[0]),\n        old_column: sym_value(args[1]),\n        new_column: sym_value(args[2])\n      }\n    end\n\n    def parse_add_index(args)\n      return unless args.size >= 2\n\n      {\n        action: :add_index,\n        table: sym_value(args[0]),\n        columns: array_or_single_value(args[1]),\n        options: parse_hash(args[2])\n      }\n    end\n\n    def parse_remove_index(args)\n      return unless args.size >= 1\n\n      {\n        action: :remove_index,\n        table: sym_value(args[0]),\n        options: parse_hash(args[1])\n      }\n    end\n\n    def parse_rename_index(args)\n      return unless args.size >= 3\n\n      {\n        action: :rename_index,\n        table: sym_value(args[0]),\n        old_name: node_value(args[1]),\n        new_name: node_value(args[2])\n      }\n    end\n\n    def parse_create_table(args)\n      return unless args.size >= 1\n\n      {\n        action: :create_table,\n        table: sym_value(args[0]),\n        options: parse_hash(args[1])\n      }\n    end\n\n    def parse_drop_table(args)\n      return unless args.size >= 1\n\n      {\n        action: :drop_table,\n        table: sym_value(args[0]),\n        options: parse_hash(args[1])\n      }\n    end\n\n    def parse_create_table_with_block(send_node, block_node)\n      args = send_node.children[2..]\n      columns = parse_create_table_columns(block_node.children[2])\n      {\n        action: :create_table,\n        table: sym_value(args[0]),\n        options: parse_hash(args[1]),\n        columns: columns\n      }\n    end\n\n    def parse_create_table_columns(body_node)\n      return [] unless body_node\n\n      nodes = body_node.type == :begin ? body_node.children : [body_node]\n      nodes.map { |node| parse_column_node(node) }.compact\n    end\n\n    def parse_column_node(node)\n      return unless node.is_a?(Parser::AST::Node) && node.type == :send\n\n      method = node.children[1]\n      return parse_timestamps if method == :timestamps\n\n      {\n        column: sym_value(node.children[2]),\n        type: method,\n        options: parse_hash(node.children[3])\n      }\n    end\n\n    def parse_timestamps\n      [\n        { column: :created_at, type: :datetime, options: { null: false } },\n        { column: :updated_at, type: :datetime, options: { null: false } }\n      ]\n    end\n\n    def sym_value(node)\n      return nil unless node && node.type == :sym\n\n      node.children.first\n    end\n\n    def array_or_single_value(node)\n      return [] unless node\n\n      if node.type == :array\n        node.children.map { |child| node_value(child) }\n      else\n        node_value(node)\n      end\n    end\n\n    def parse_hash(node)\n      return {} unless node && node.type == :hash\n\n      node.children.each_with_object({}) do |pair_node, result|\n        key_node, value_node = pair_node.children\n        key = sym_value(key_node) || node_value(key_node)\n        value = node_value(value_node)\n        result[key] = value\n      end\n    end\n\n    def node_value(node)\n      return nil unless node\n\n      case node.type\n      when :str, :sym, :int then node.children.first\n      when true then true\n      when false then false\n      when nil then nil\n      else\n        node.children.first\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/actual_db_schema/multi_tenant.rb",
    "content": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n  # Handles multi-tenancy support by switching schemas for supported databases\n  module MultiTenant\n    include ActualDbSchema::OutputFormatter\n\n    class << self\n      def with_schema(schema_name)\n        context = switch_schema(schema_name)\n        yield\n      ensure\n        restore_context(context)\n      end\n\n      private\n\n      def adapter_name\n        ActiveRecord::Base.connection.adapter_name\n      end\n\n      def switch_schema(schema_name)\n        case adapter_name\n        when /postgresql/i\n          switch_postgresql_schema(schema_name)\n        when /mysql/i\n          switch_mysql_schema(schema_name)\n        else\n          message = \"[ActualDbSchema] Multi-tenancy not supported for adapter: #{adapter_name}. \" \\\n            \"Proceeding without schema switching.\"\n          puts colorize(message, :gray)\n        end\n      end\n\n      def switch_postgresql_schema(schema_name)\n        old_search_path = ActiveRecord::Base.connection.schema_search_path\n        ActiveRecord::Base.connection.schema_search_path = schema_name\n        { type: :postgresql, old_context: old_search_path }\n      end\n\n      def switch_mysql_schema(schema_name)\n        old_db = ActiveRecord::Base.connection.current_database\n        ActiveRecord::Base.connection.execute(\"USE #{ActiveRecord::Base.connection.quote_table_name(schema_name)}\")\n        { type: :mysql, old_context: old_db }\n      end\n\n      def restore_context(context)\n        return unless context\n\n        case context[:type]\n        when :postgresql\n          ActiveRecord::Base.connection.schema_search_path = context[:old_context] if context[:old_context]\n        when :mysql\n          return unless context[:old_context]\n\n          ActiveRecord::Base.connection.execute(\n            \"USE #{ActiveRecord::Base.connection.quote_table_name(context[:old_context])}\"\n          )\n        end\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/actual_db_schema/output_formatter.rb",
    "content": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n  # Provides functionality for formatting terminal output with colors\n  module OutputFormatter\n    UNICODE_COLORS = {\n      red: 31,\n      green: 32,\n      yellow: 33,\n      gray: 90\n    }.freeze\n\n    def colorize(text, color)\n      code = UNICODE_COLORS.fetch(color, 37)\n      \"\\e[#{code}m#{text}\\e[0m\"\n    end\n  end\nend\n"
  },
  {
    "path": "lib/actual_db_schema/patches/migration_context.rb",
    "content": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n  module Patches\n    # Add new command to roll back the phantom migrations\n    module MigrationContext\n      include ActualDbSchema::OutputFormatter\n\n      def rollback_branches(manual_mode: false)\n        schemas = multi_tenant_schemas&.call || []\n        schema_count = schemas.any? ? schemas.size : 1\n\n        rolled_back_migrations = if schemas.any?\n                                   rollback_multi_tenant(schemas, manual_mode: manual_mode)\n                                 else\n                                   rollback_branches_for_schema(manual_mode: manual_mode)\n                                 end\n\n        delete_migrations(rolled_back_migrations, schema_count)\n        rolled_back_migrations.any?\n      end\n\n      def phantom_migrations\n        paths = Array(migrations_paths)\n        current_branch_files = Dir[*paths.flat_map { |path| \"#{path}/**/[0-9]*_*.rb\" }]\n        current_branch_file_names = current_branch_files.map { |f| ActualDbSchema.migration_filename(f) }\n\n        migrations.reject do |migration|\n          current_branch_file_names.include?(ActualDbSchema.migration_filename(migration.filename))\n        end\n      end\n\n      private\n\n      def rollback_branches_for_schema(manual_mode: false, schema_name: nil, rolled_back_migrations: [])\n        phantom_migrations.reverse_each do |migration|\n          next unless status_up?(migration)\n\n          show_info_for(migration, schema_name) if manual_mode\n          if !manual_mode || user_wants_rollback?\n            migrate(migration, rolled_back_migrations, schema_name,\n                    manual_mode: manual_mode)\n          end\n        rescue StandardError => e\n          handle_rollback_error(migration, e, schema_name)\n        end\n\n        rolled_back_migrations\n      end\n\n      def rollback_multi_tenant(schemas, manual_mode: false)\n        all_rolled_back_migrations = []\n\n        schemas.each do |schema_name|\n          ActualDbSchema::MultiTenant.with_schema(schema_name) do\n            rollback_branches_for_schema(manual_mode: manual_mode, schema_name: schema_name,\n                                         rolled_back_migrations: all_rolled_back_migrations)\n          end\n        end\n\n        all_rolled_back_migrations\n      end\n\n      def down_migrator_for(migration)\n        if ActiveRecord::Migration.current_version < 6\n          ActiveRecord::Migrator.new(:down, [migration], migration.version)\n        elsif ActiveRecord::Migration.current_version < 7.1\n          ActiveRecord::Migrator.new(:down, [migration], schema_migration, migration.version)\n        else\n          ActiveRecord::Migrator.new(:down, [migration], schema_migration, internal_metadata, migration.version)\n        end\n      end\n\n      def migration_files\n        paths = Array(migrations_paths)\n        current_branch_files = Dir[*paths.flat_map { |path| \"#{path}/**/[0-9]*_*.rb\" }]\n        other_branches_files = ActualDbSchema::Store.instance.migration_files\n        current_branch_versions = current_branch_files.map { |file| file.match(/(\\d+)_/)[1] }\n        filtered_other_branches_files = other_branches_files.reject do |file|\n          version = file.match(/(\\d+)_/)[1]\n          current_branch_versions.include?(version)\n        end\n\n        current_branch_files + filtered_other_branches_files\n      end\n\n      def status_up?(migration)\n        migrations_status.any? do |status, version|\n          status == \"up\" && version.to_s == migration.version.to_s\n        end\n      end\n\n      def user_wants_rollback?\n        print \"\\nRollback this migration? [y,n] \"\n        answer = $stdin.gets.chomp.downcase\n        answer[0] == \"y\"\n      end\n\n      def show_info_for(migration, schema_name = nil)\n        puts colorize(\"\\n[ActualDbSchema] A phantom migration was found and is about to be rolled back.\", :gray)\n        puts \"Please make a decision from the options below to proceed.\\n\\n\"\n        puts \"Schema: #{schema_name}\" if schema_name\n        puts \"Branch: #{branch_for(migration.version.to_s)}\"\n        puts \"Database: #{ActualDbSchema.db_config[:database]}\"\n        puts \"Version: #{migration.version}\\n\\n\"\n        puts File.read(migration.filename)\n      end\n\n      def migrate(migration, rolled_back_migrations, schema_name = nil, manual_mode: false)\n        migration.name = extract_class_name(migration.filename)\n\n        branch = branch_for(migration.version.to_s)\n        message = \"[ActualDbSchema]\"\n        message += \" #{schema_name}:\" if schema_name\n        message += \" Rolling back phantom migration #{migration.version} #{migration.name} \" \\\n                   \"(from branch: #{branch})\"\n        puts colorize(message, :gray)\n\n        migrator = down_migrator_for(migration)\n        migrator.extend(ActualDbSchema::Patches::Migrator)\n        migrator.migrate\n        notify_rollback_migration(migration: migration, schema_name: schema_name, branch: branch,\n                                  manual_mode: manual_mode)\n        rolled_back_migrations << migration\n      end\n\n      def notify_rollback_migration(migration:, schema_name:, branch:, manual_mode:)\n        ActiveSupport::Notifications.instrument(\n          ActualDbSchema::Instrumentation::ROLLBACK_EVENT,\n          version: migration.version.to_s,\n          name: migration.name,\n          database: ActualDbSchema.db_config[:database],\n          schema: schema_name,\n          branch: branch,\n          manual_mode: manual_mode\n        )\n      end\n\n      def extract_class_name(filename)\n        content = File.read(filename)\n        content.match(/^class\\s+([A-Za-z0-9_]+)\\s+</)[1]\n      end\n\n      def branch_for(version)\n        metadata.fetch(version, {})[:branch] || \"unknown\"\n      end\n\n      def metadata\n        @metadata ||= ActualDbSchema::Store.instance.read\n      end\n\n      def handle_rollback_error(migration, exception, schema_name = nil)\n        error_message = <<~ERROR\n          Error encountered during rollback:\n\n          #{cleaned_exception_message(exception.message)}\n        ERROR\n\n        puts colorize(error_message, :red)\n        ActualDbSchema.failed << FailedMigration.new(\n          migration: migration,\n          exception: exception,\n          branch: branch_for(migration.version.to_s),\n          schema: schema_name\n        )\n      end\n\n      def cleaned_exception_message(message)\n        patterns_to_remove = [\n          /^An error has occurred, all later migrations canceled:\\s*/,\n          /^An error has occurred, this and all later migrations canceled:\\s*/\n        ]\n\n        patterns_to_remove.reduce(message.strip) { |msg, pattern| msg.gsub(pattern, \"\").strip }\n      end\n\n      def delete_migrations(migrations, schema_count)\n        migration_counts = migrations.each_with_object(Hash.new(0)) do |migration, hash|\n          hash[migration.filename] += 1\n        end\n\n        migrations.uniq.each do |migration|\n          count = migration_counts[migration.filename]\n          ActualDbSchema::Store.instance.delete(migration.filename) if count == schema_count\n        end\n      end\n\n      def multi_tenant_schemas\n        ActualDbSchema.config[:multi_tenant_schemas]\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/actual_db_schema/patches/migration_proxy.rb",
    "content": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n  module Patches\n    # Records the migration file into the tmp folder after it's been migrated\n    module MigrationProxy\n      def migrate(direction)\n        super(direction)\n        ActualDbSchema::Store.instance.write(filename) if direction == :up\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/actual_db_schema/patches/migrator.rb",
    "content": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n  module Patches\n    # Run only one migration that's being rolled back\n    module Migrator\n      def runnable\n        migration = migrations.first # there is only one migration, because we pass only one here\n        ran?(migration) ? [migration] : []\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/actual_db_schema/railtie.rb",
    "content": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n  # Integrates the ConsoleMigrations module into the Rails console.\n  class Railtie < ::Rails::Railtie\n    console do\n      require_relative \"console_migrations\"\n\n      if ActualDbSchema.config[:console_migrations_enabled]\n        TOPLEVEL_BINDING.receiver.extend(ActualDbSchema::ConsoleMigrations)\n        puts \"[ActualDbSchema] ConsoleMigrations enabled. You can now use migration methods directly at the console.\"\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/actual_db_schema/rollback_stats_repository.rb",
    "content": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n  # Persists rollback events in DB.\n  class RollbackStatsRepository\n    TABLE_NAME = \"actual_db_schema_rollback_events\"\n\n    class << self\n      def record(payload)\n        ensure_table!\n        connection.execute(<<~SQL.squish)\n          INSERT INTO #{quoted_table}\n            (#{quoted_column(\"version\")}, #{quoted_column(\"name\")}, #{quoted_column(\"database\")},\n             #{quoted_column(\"schema\")}, #{quoted_column(\"branch\")}, #{quoted_column(\"manual_mode\")},\n             #{quoted_column(\"created_at\")})\n          VALUES\n            (#{connection.quote(payload[:version].to_s)}, #{connection.quote(payload[:name].to_s)},\n             #{connection.quote(payload[:database].to_s)}, #{connection.quote((payload[:schema] || \"default\").to_s)},\n             #{connection.quote(payload[:branch].to_s)}, #{connection.quote(!!payload[:manual_mode])},\n             #{connection.quote(Time.current)})\n        SQL\n      end\n\n      def stats\n        return empty_stats unless table_exists?\n\n        {\n          total: total_rollbacks,\n          by_database: aggregate_by(:database),\n          by_schema: aggregate_by(:schema),\n          by_branch: aggregate_by(:branch)\n        }\n      end\n\n      def total_rollbacks\n        return 0 unless table_exists?\n\n        connection.select_value(<<~SQL.squish).to_i\n          SELECT COUNT(*) FROM #{quoted_table}\n        SQL\n      end\n\n      def reset!\n        return unless table_exists?\n\n        connection.execute(\"DELETE FROM #{quoted_table}\")\n      end\n\n      private\n\n      def ensure_table!\n        return if table_exists?\n\n        connection.create_table(TABLE_NAME) do |t|\n          t.string :version, null: false\n          t.string :name\n          t.string :database, null: false\n          t.string :schema\n          t.string :branch, null: false\n          t.boolean :manual_mode, null: false, default: false\n          t.datetime :created_at, null: false\n        end\n      end\n\n      def table_exists?\n        connection.table_exists?(TABLE_NAME)\n      end\n\n      def aggregate_by(column)\n        return {} unless table_exists?\n\n        rows = connection.select_all(<<~SQL.squish)\n          SELECT #{quoted_column(column)}, COUNT(*) AS cnt\n          FROM #{quoted_table}\n          GROUP BY #{quoted_column(column)}\n        SQL\n        rows.each_with_object(Hash.new(0)) { |row, h| h[row[column.to_s].to_s] = row[\"cnt\"].to_i }\n      end\n\n      def empty_stats\n        {\n          total: 0,\n          by_database: {},\n          by_schema: {},\n          by_branch: {}\n        }\n      end\n\n      def connection\n        ActiveRecord::Base.connection\n      end\n\n      def quoted_table\n        connection.quote_table_name(TABLE_NAME)\n      end\n\n      def quoted_column(name)\n        connection.quote_column_name(name)\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/actual_db_schema/schema_diff.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"tempfile\"\n\nmodule ActualDbSchema\n  # Generates a diff of schema changes between the current schema file and the\n  # last committed version, annotated with the migrations responsible for each change.\n  class SchemaDiff\n    include OutputFormatter\n\n    SIGN_COLORS = {\n      \"+\" => :green,\n      \"-\" => :red\n    }.freeze\n\n    CHANGE_PATTERNS = {\n      /t\\.(\\w+)\\s+[\"']([^\"']+)[\"']/ => :column,\n      /t\\.index\\s+.*name:\\s*[\"']([^\"']+)[\"']/ => :index,\n      /create_table\\s+[\"']([^\"']+)[\"']/ => :table\n    }.freeze\n\n    SQL_CHANGE_PATTERNS = {\n      /CREATE (?:UNIQUE\\s+)?INDEX\\s+[\"']?([^\"'\\s]+)[\"']?\\s+ON\\s+([\\w.]+)/i => :index,\n      /CREATE TABLE\\s+(\\S+)\\s+\\(/i => :table,\n      /CREATE SEQUENCE\\s+(\\S+)/i => :table,\n      /ALTER SEQUENCE\\s+(\\S+)\\s+OWNED BY\\s+([\\w.]+)/i => :table,\n      /ALTER TABLE\\s+ONLY\\s+(\\S+)\\s+/i => :table\n    }.freeze\n\n    def initialize(schema_path, migrations_path)\n      @schema_path = schema_path\n      @migrations_path = migrations_path\n    end\n\n    def render\n      if old_schema_content.nil? || old_schema_content.strip.empty?\n        puts colorize(\"Could not retrieve old schema from git.\", :red)\n        return\n      end\n\n      diff_output = generate_diff(old_schema_content, new_schema_content)\n      process_diff_output(diff_output)\n    end\n\n    private\n\n    def old_schema_content\n      @old_schema_content ||= begin\n        output = `git show HEAD:#{@schema_path} 2>&1`\n        $CHILD_STATUS.success? ? output : nil\n      end\n    end\n\n    def new_schema_content\n      @new_schema_content ||= File.read(@schema_path)\n    end\n\n    def parsed_old_schema\n      @parsed_old_schema ||= parser_class.parse_string(old_schema_content.to_s)\n    end\n\n    def parsed_new_schema\n      @parsed_new_schema ||= parser_class.parse_string(new_schema_content.to_s)\n    end\n\n    def parser_class\n      structure_sql? ? StructureSqlParser : SchemaParser\n    end\n\n    def structure_sql?\n      File.extname(@schema_path) == \".sql\"\n    end\n\n    def migration_changes\n      @migration_changes ||= begin\n        migration_dirs = [@migrations_path] + migrated_folders\n        MigrationParser.parse_all_migrations(migration_dirs)\n      end\n    end\n\n    def migrated_folders\n      ActualDbSchema::Store.instance.materialize_all\n      dirs = find_migrated_folders\n\n      configured_migrated_folder = ActualDbSchema.migrated_folder\n      relative_migrated_folder = configured_migrated_folder.to_s.sub(%r{\\A#{Regexp.escape(Rails.root.to_s)}/?}, \"\")\n      dirs << relative_migrated_folder unless dirs.include?(relative_migrated_folder)\n\n      dirs.map { |dir| dir.sub(%r{\\A\\./}, \"\") }.uniq\n    end\n\n    def find_migrated_folders\n      path_parts = Pathname.new(@migrations_path).each_filename.to_a\n      db_index = path_parts.index(\"db\")\n      return [] unless db_index\n\n      base_path = db_index.zero? ? \".\" : File.join(*path_parts[0...db_index])\n      Dir[File.join(base_path, \"tmp\", \"migrated*\")].select do |path|\n        File.directory?(path) && File.basename(path).match?(/^migrated(_[a-zA-Z0-9_-]+)?$/)\n      end\n    end\n\n    def generate_diff(old_content, new_content)\n      Tempfile.create(\"old_schema\") do |old_file|\n        Tempfile.create(\"new_schema\") do |new_file|\n          old_file.write(old_content)\n          new_file.write(new_content)\n          old_file.rewind\n          new_file.rewind\n\n          return `diff -u #{old_file.path} #{new_file.path}`\n        end\n      end\n    end\n\n    def process_diff_output(diff_str)\n      lines = diff_str.lines\n      current_table = nil\n      result_lines  = []\n\n      lines.each do |line|\n        if (hunk_match = line.match(/^@@\\s+-(\\d+),(\\d+)\\s+\\+(\\d+),(\\d+)\\s+@@/))\n          current_table = find_table_in_new_schema(hunk_match[3].to_i)\n        elsif (ct = line.match(/create_table\\s+[\"']([^\"']+)[\"']/) ||\n          line.match(/CREATE TABLE\\s+\"?([^\"\\s]+)\"?/i) || line.match(/ALTER TABLE\\s+ONLY\\s+(\\S+)/i))\n          current_table = normalize_table_name(ct[1])\n        end\n\n        result_lines << (%w[+ -].include?(line[0]) ? handle_diff_line(line, current_table) : line)\n      end\n\n      result_lines.join\n    end\n\n    def handle_diff_line(line, current_table)\n      sign = line[0]\n      line_content = line[1..]\n      color = SIGN_COLORS[sign]\n\n      action, name = detect_action_and_name(line_content, sign, current_table)\n      annotation = action ? find_migrations(action, current_table, name) : []\n      annotated_line = annotation.any? ? annotate_line(line, annotation) : line\n\n      colorize(annotated_line, color)\n    end\n\n    def detect_action_and_name(line_content, sign, current_table)\n      patterns = structure_sql? ? SQL_CHANGE_PATTERNS : CHANGE_PATTERNS\n      action_map = {\n        column: ->(md) { [guess_action(sign, current_table, md[2]), md[2]] },\n        index: ->(md) { [sign == \"+\" ? :add_index : :remove_index, md[1]] },\n        table: ->(_) { [sign == \"+\" ? :create_table : :drop_table, nil] }\n      }\n\n      patterns.each do |regex, kind|\n        next unless (md = line_content.match(regex))\n\n        action_proc = action_map[kind]\n        return action_proc.call(md) if action_proc\n      end\n\n      if structure_sql? && current_table && (md = line_content.match(/^\\s*\"?(\\w+)\"?\\s+(.+?)(?:,|\\s*$)/i))\n        return [guess_action(sign, current_table, md[1]), md[1]]\n      end\n\n      [nil, nil]\n    end\n\n    def guess_action(sign, table, col_name)\n      case sign\n      when \"+\"\n        old_table = parsed_old_schema[table] || {}\n        old_table[col_name].nil? ? :add_column : :change_column\n      when \"-\"\n        new_table = parsed_new_schema[table] || {}\n        new_table[col_name].nil? ? :remove_column : :change_column\n      end\n    end\n\n    def find_table_in_new_schema(new_line_number)\n      current_table = nil\n\n      new_schema_content.lines[0...new_line_number].each do |line|\n        if (match = line.match(/create_table\\s+[\"']([^\"']+)[\"']/) || line.match(/CREATE TABLE\\s+\"?([^\"\\s]+)\"?/i))\n          current_table = normalize_table_name(match[1])\n        end\n      end\n      current_table\n    end\n\n    def find_migrations(action, table_name, col_or_index_name)\n      matches = []\n\n      migration_changes.each do |file_path, changes|\n        changes.each do |chg|\n          next unless (structure_sql? && index_action?(action)) || chg[:table].to_s == table_name.to_s\n\n          matches << file_path if migration_matches?(chg, action, col_or_index_name)\n        end\n      end\n\n      matches\n    end\n\n    def index_action?(action)\n      %i[add_index remove_index rename_index].include?(action)\n    end\n\n    def migration_matches?(chg, action, col_or_index_name)\n      return (chg[:action] == action) if col_or_index_name.nil?\n\n      matchers = {\n        rename_column: -> { rename_column_matches?(chg, action, col_or_index_name) },\n        rename_index: -> { rename_index_matches?(chg, action, col_or_index_name) },\n        add_index: -> { index_matches?(chg, action, col_or_index_name) },\n        remove_index: -> { index_matches?(chg, action, col_or_index_name) }\n      }\n\n      matchers.fetch(chg[:action], -> { column_matches?(chg, action, col_or_index_name) }).call\n    end\n\n    def rename_column_matches?(chg, action, col)\n      (action == :remove_column && chg[:old_column].to_s == col.to_s) ||\n        (action == :add_column && chg[:new_column].to_s == col.to_s)\n    end\n\n    def rename_index_matches?(chg, action, name)\n      (action == :remove_index && chg[:old_name] == name) ||\n        (action == :add_index && chg[:new_name] == name)\n    end\n\n    def index_matches?(chg, action, col_or_index_name)\n      return false unless chg[:action] == action\n\n      extract_migration_index_name(chg, chg[:table]) == col_or_index_name.to_s\n    end\n\n    def column_matches?(chg, action, col_name)\n      chg[:column] && chg[:column].to_s == col_name.to_s && chg[:action] == action\n    end\n\n    def extract_migration_index_name(chg, table_name)\n      return chg[:options][:name].to_s if chg[:options].is_a?(Hash) && chg[:options][:name]\n\n      return \"\" unless (columns = chg[:columns])\n\n      cols = columns.is_a?(Array) ? columns : [columns]\n      \"index_#{table_name}_on_#{cols.join(\"_and_\")}\"\n    end\n\n    def annotate_line(line, migration_file_paths)\n      \"#{line.chomp}#{colorize(\" // #{migration_file_paths.join(\", \")} //\", :gray)}\\n\"\n    end\n\n    def normalize_table_name(table_name)\n      return table_name unless structure_sql? && table_name.include?(\".\")\n\n      table_name.split(\".\").last\n    end\n  end\nend\n"
  },
  {
    "path": "lib/actual_db_schema/schema_diff_html.rb",
    "content": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n  # Generates an HTML representation of the schema diff,\n  # annotated with the migrations responsible for each change.\n  class SchemaDiffHtml < SchemaDiff\n    def render_html(table_filter)\n      return unless old_schema_content && !old_schema_content.strip.empty?\n\n      @full_diff_html ||= generate_diff_html\n      filter = table_filter.to_s.strip.downcase\n\n      filter.empty? ? @full_diff_html : extract_table_section(@full_diff_html, filter)\n    end\n\n    private\n\n    def generate_diff_html\n      diff_output = generate_full_diff(old_schema_content, new_schema_content)\n      diff_output = new_schema_content if diff_output.strip.empty?\n\n      process_diff_output_for_html(diff_output)\n    end\n\n    def generate_full_diff(old_content, new_content)\n      Tempfile.create(\"old_schema\") do |old_file|\n        Tempfile.create(\"new_schema\") do |new_file|\n          old_file.write(old_content)\n          new_file.write(new_content)\n          old_file.rewind\n          new_file.rewind\n\n          `diff -u -U 9999999 #{old_file.path} #{new_file.path}`\n        end\n      end\n    end\n\n    def process_diff_output_for_html(diff_str)\n      current_table = nil\n      result_lines = []\n      @tables = {}\n      table_start = nil\n      block_depth = 1\n\n      diff_str.lines.each do |line|\n        next if skip_line?(line)\n\n        current_table, table_start, block_depth =\n          process_table(line, current_table, table_start, result_lines.size, block_depth)\n        result_lines << (%w[+ -].include?(line[0]) ? handle_diff_line_html(line, current_table) : line)\n      end\n\n      result_lines.join\n    end\n\n    def skip_line?(line)\n      line != \"---\\n\" && !line.match(/^--- Name/) &&\n        (line.start_with?(\"---\") || line.start_with?(\"+++\") || line.match(/^@@/))\n    end\n\n    def process_table(line, current_table, table_start, table_end, block_depth)\n      if (ct = line.match(/create_table\\s+[\"']([^\"']+)[\"']/) || line.match(/CREATE TABLE\\s+\"?([^\"\\s]+)\"?/i))\n        return [normalize_table_name(ct[1]), table_end, block_depth]\n      end\n\n      return [current_table, table_start, block_depth] unless current_table\n\n      block_depth += line.scan(/\\bdo\\b/).size unless line.match(/create_table\\s+[\"']([^\"']+)[\"']/)\n      block_depth -= line.scan(/\\bend\\b/).size\n      block_depth -= line.scan(/\\);\\s*$/).size\n\n      if block_depth.zero?\n        @tables[current_table] = { start: table_start, end: table_end }\n        current_table = nil\n        block_depth = 1\n      end\n\n      [current_table, table_start, block_depth]\n    end\n\n    def handle_diff_line_html(line, current_table)\n      sign = line[0]\n      line_content = line[1..]\n      color = SIGN_COLORS[sign]\n\n      action, name = detect_action_and_name(line_content, sign, current_table)\n      annotation = action ? find_migrations(action, current_table, name) : []\n      annotation.any? ? annotate_line(line, annotation, color) : colorize_html(line, color)\n    end\n\n    def annotate_line(line, migration_file_paths, color)\n      links_html = migration_file_paths.map { |path| link_to_migration(path) }.join(\", \")\n      \"#{colorize_html(line.chomp, color)}#{colorize_html(\" // #{links_html} //\", :gray)}\\n\"\n    end\n\n    def colorize_html(text, color)\n      safe = ERB::Util.html_escape(text)\n\n      case color\n      when :green\n        %(<span style=\"color: green\">#{safe}</span>)\n      when :red\n        %(<span style=\"color: red\">#{safe}</span>)\n      when :gray\n        %(<span style=\"color: gray\">#{text}</span>)\n      end\n    end\n\n    def link_to_migration(migration_file_path)\n      migration = migrations.detect { |m| File.expand_path(m.filename) == File.expand_path(migration_file_path) }\n      return ERB::Util.html_escape(migration_file_path) unless migration\n\n      url = \"migrations/#{migration.version}?database=#{migration.database}\"\n      \"<a href=\\\"#{url}\\\">#{ERB::Util.html_escape(migration_file_path)}</a>\"\n    end\n\n    def migrations\n      @migrations ||= ActualDbSchema::Migration.instance.all\n    end\n\n    def extract_table_section(full_diff_html, table_name)\n      return unless @tables[table_name]\n\n      range = @tables[table_name]\n      full_diff_html.lines[range[:start]..range[:end]].join\n    end\n  end\nend\n"
  },
  {
    "path": "lib/actual_db_schema/schema_parser.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"parser/ast/processor\"\nrequire \"prism\"\n\nmodule ActualDbSchema\n  # Parses the content of a `schema.rb` file into a structured hash representation.\n  module SchemaParser\n    module_function\n\n    def parse_string(schema_content)\n      ast = Prism::Translation::Parser.parse(schema_content)\n\n      collector = SchemaCollector.new\n      collector.process(ast)\n      collector.schema\n    end\n\n    # Internal class used to process the AST and collect schema information.\n    class SchemaCollector < Parser::AST::Processor\n      attr_reader :schema\n\n      def initialize\n        super()\n        @schema = {}\n      end\n\n      def on_block(node)\n        send_node, _args_node, body = *node\n\n        if create_table_call?(send_node)\n          table_name = extract_table_name(send_node)\n          columns    = extract_columns(body)\n          @schema[table_name] = columns if table_name\n        end\n\n        super\n      end\n\n      def on_send(node)\n        _receiver, method_name, *args = *node\n        if method_name == :create_table && args.any?\n          table_name = extract_table_name(node)\n          @schema[table_name] ||= {}\n        end\n\n        super\n      end\n\n      private\n\n      def create_table_call?(node)\n        return false unless node.is_a?(Parser::AST::Node)\n\n        _receiver, method_name, *_args = node.children\n        method_name == :create_table\n      end\n\n      def extract_table_name(send_node)\n        _receiver, _method_name, table_arg, *_rest = send_node.children\n        return unless table_arg\n\n        case table_arg.type\n        when :str then table_arg.children.first\n        when :sym then table_arg.children.first.to_s\n        end\n      end\n\n      def extract_columns(body_node)\n        return {} unless body_node\n\n        children = body_node.type == :begin ? body_node.children : [body_node]\n\n        columns = {}\n        children.each do |expr|\n          col = process_column_node(expr)\n          columns[col[:name]] = { type: col[:type], options: col[:options] } if col && col[:name]\n        end\n        columns\n      end\n\n      def process_column_node(node)\n        return unless node.is_a?(Parser::AST::Node)\n        return unless node.type == :send\n\n        receiver, method_name, column_node, *args = node.children\n\n        return unless receiver && receiver.type == :lvar\n\n        return { name: \"timestamps\", type: :timestamps, options: {} } if method_name == :timestamps\n\n        col_name = extract_column_name(column_node)\n        options  = extract_column_options(args)\n\n        { name: col_name, type: method_name, options: options }\n      end\n\n      def extract_column_name(node)\n        return nil unless node.is_a?(Parser::AST::Node)\n\n        case node.type\n        when :str then node.children.first\n        when :sym then node.children.first.to_s\n        end\n      end\n\n      def extract_column_options(args)\n        opts = {}\n        args.each do |arg|\n          next unless arg && arg.type == :hash\n\n          opts.merge!(parse_hash(arg))\n        end\n        opts\n      end\n\n      def parse_hash(node)\n        hash = {}\n        return hash unless node && node.type == :hash\n\n        node.children.each do |pair|\n          key_node, value_node = pair.children\n          key = extract_key(key_node)\n          value = extract_literal(value_node)\n          hash[key] = value\n        end\n        hash\n      end\n\n      def extract_key(node)\n        return unless node.is_a?(Parser::AST::Node)\n\n        case node.type\n        when :sym then node.children.first\n        when :str then node.children.first.to_sym\n        end\n      end\n\n      def extract_literal(node)\n        return unless node.is_a?(Parser::AST::Node)\n\n        case node.type\n        when :int, :str, :sym then node.children.first\n        when true then true\n        when false then false\n        end\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/actual_db_schema/store.rb",
    "content": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n  # Stores migration sources and metadata.\n  class Store\n    include Singleton\n\n    Item = Struct.new(:version, :timestamp, :branch)\n\n    def write(filename)\n      adapter.write(filename)\n      reset_source_cache\n    end\n\n    def read\n      adapter.read\n    end\n\n    def migration_files\n      adapter.migration_files\n    end\n\n    def delete(filename)\n      adapter.delete(filename)\n      reset_source_cache\n    end\n\n    def stored_migration?(filename)\n      adapter.stored_migration?(filename)\n    end\n\n    def source_for(version)\n      version = version.to_s\n\n      return :db if db_versions.key?(version)\n      return :file if file_versions.key?(version)\n\n      :unknown\n    end\n\n    def materialize_all\n      adapter.materialize_all\n    end\n\n    def reset_adapter\n      @adapter = nil\n      reset_source_cache\n    end\n\n    private\n\n    def adapter\n      @adapter ||= begin\n        storage = ActualDbSchema.config[:migrations_storage].to_s\n        storage == \"db\" ? DbAdapter.new : FileAdapter.new\n      end\n    end\n\n    def reset_source_cache\n      @db_versions = nil\n      @file_versions = nil\n    end\n\n    def db_versions\n      @db_versions ||= begin\n        connection = ActiveRecord::Base.connection\n        return {} unless connection.table_exists?(DbAdapter::TABLE_NAME)\n\n        table = connection.quote_table_name(DbAdapter::TABLE_NAME)\n        connection.select_values(\"SELECT version FROM #{table}\").each_with_object({}) do |version, acc|\n          acc[version.to_s] = true\n        end\n      rescue StandardError\n        {}\n      end\n    end\n\n    def file_versions\n      @file_versions ||= FileAdapter.new.read\n    rescue StandardError\n      {}\n    end\n\n    # Stores migrated files on the filesystem with metadata in CSV.\n    class FileAdapter\n      def write(filename)\n        basename = File.basename(filename)\n        FileUtils.mkdir_p(folder)\n        FileUtils.copy(filename, folder.join(basename))\n        record_metadata(filename)\n      end\n\n      def read\n        return {} unless File.exist?(store_file)\n\n        CSV.read(store_file).map { |line| Item.new(*line) }.index_by(&:version)\n      end\n\n      def migration_files\n        Dir[\"#{folder}/**/[0-9]*_*.rb\"]\n      end\n\n      def delete(filename)\n        File.delete(filename) if File.exist?(filename)\n      end\n\n      def stored_migration?(filename)\n        filename.to_s.start_with?(folder.to_s)\n      end\n\n      def materialize_all\n        nil\n      end\n\n      private\n\n      def record_metadata(filename)\n        version = File.basename(filename).scan(/(\\d+)_.*\\.rb/).first.first\n        CSV.open(store_file, \"a\") do |csv|\n          csv << [\n            version,\n            Time.current.iso8601,\n            Git.current_branch\n          ]\n        end\n      end\n\n      def folder\n        ActualDbSchema.migrated_folder\n      end\n\n      def store_file\n        folder.join(\"metadata.csv\")\n      end\n    end\n\n    # Stores migrated files in the database.\n    class DbAdapter\n      TABLE_NAME = \"actual_db_schema_migrations\"\n      RECORD_COLUMNS = %w[version filename content branch migrated_at].freeze\n\n      def write(filename)\n        ensure_table!\n\n        version = extract_version(filename)\n        return unless version\n\n        basename = File.basename(filename)\n        content = File.read(filename)\n        upsert_record(version, basename, content, Git.current_branch, Time.current)\n        write_cache_file(basename, content)\n      end\n\n      def read\n        return {} unless table_exists?\n\n        rows = connection.exec_query(<<~SQL.squish)\n          SELECT version, migrated_at, branch\n          FROM #{quoted_table}\n        SQL\n\n        rows.map do |row|\n          Item.new(row[\"version\"].to_s, row[\"migrated_at\"], row[\"branch\"])\n        end.index_by(&:version)\n      end\n\n      def migration_files\n        materialize_all\n        Dir[\"#{folder}/**/[0-9]*_*.rb\"]\n      end\n\n      def delete(filename)\n        version = extract_version(filename)\n        return unless version\n\n        if table_exists?\n          connection.execute(<<~SQL.squish)\n            DELETE FROM #{quoted_table}\n            WHERE #{quoted_column(\"version\")} = #{connection.quote(version)}\n          SQL\n        end\n        File.delete(filename) if File.exist?(filename)\n      end\n\n      def stored_migration?(filename)\n        filename.to_s.start_with?(folder.to_s)\n      end\n\n      def materialize_all\n        return unless table_exists?\n\n        FileUtils.mkdir_p(folder)\n        rows = connection.exec_query(<<~SQL.squish)\n          SELECT filename, content\n          FROM #{quoted_table}\n        SQL\n\n        rows.each do |row|\n          write_cache_file(row[\"filename\"], row[\"content\"])\n        end\n      end\n\n      private\n\n      def upsert_record(version, basename, content, branch, migrated_at)\n        attributes = record_attributes(version, basename, content, branch, migrated_at)\n        record_exists?(version) ? update_record(attributes) : insert_record(attributes)\n      end\n\n      def record_attributes(version, basename, content, branch, migrated_at)\n        {\n          version: version,\n          filename: basename,\n          content: content,\n          branch: branch,\n          migrated_at: migrated_at\n        }\n      end\n\n      def update_record(attributes)\n        assignments = record_columns.reject { |column| column == \"version\" }.map do |column|\n          \"#{quoted_column(column)} = #{connection.quote(attributes[column.to_sym])}\"\n        end\n\n        connection.execute(<<~SQL)\n          UPDATE #{quoted_table}\n          SET #{assignments.join(\", \")}\n          WHERE #{quoted_column(\"version\")} = #{connection.quote(attributes[:version])}\n        SQL\n      end\n\n      def insert_record(attributes)\n        columns = record_columns\n        values = columns.map { |column| connection.quote(attributes[column.to_sym]) }\n\n        connection.execute(<<~SQL)\n          INSERT INTO #{quoted_table}\n            (#{columns.map { |column| quoted_column(column) }.join(\", \")})\n          VALUES\n            (#{values.join(\", \")})\n        SQL\n      end\n\n      def record_exists?(version)\n        connection.select_value(<<~SQL.squish).present?\n          SELECT 1\n          FROM #{quoted_table}\n          WHERE #{quoted_column(\"version\")} = #{connection.quote(version)}\n          LIMIT 1\n        SQL\n      end\n\n      def ensure_table!\n        return if table_exists?\n\n        connection.create_table(TABLE_NAME) do |t|\n          t.string :version, null: false\n          t.string :filename, null: false\n          t.text :content, null: false\n          t.string :branch\n          t.datetime :migrated_at, null: false\n        end\n\n        connection.add_index(TABLE_NAME, :version, unique: true) unless connection.index_exists?(TABLE_NAME, :version)\n      end\n\n      def table_exists?\n        connection.table_exists?(TABLE_NAME)\n      end\n\n      def connection\n        ActiveRecord::Base.connection\n      end\n\n      def record_columns\n        RECORD_COLUMNS\n      end\n\n      def quoted_table\n        connection.quote_table_name(TABLE_NAME)\n      end\n\n      def quoted_column(name)\n        connection.quote_column_name(name)\n      end\n\n      def folder\n        ActualDbSchema.migrated_folder\n      end\n\n      def write_cache_file(filename, content)\n        FileUtils.mkdir_p(folder)\n        path = folder.join(File.basename(filename))\n        return if File.exist?(path) && File.read(path) == content\n\n        File.write(path, content)\n      end\n\n      def extract_version(filename)\n        match = File.basename(filename).scan(/(\\d+)_.*\\.rb/).first\n        match&.first\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/actual_db_schema/structure_sql_parser.rb",
    "content": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n  # Parses the content of a `structure.sql` file into a structured hash representation.\n  module StructureSqlParser\n    module_function\n\n    def parse_string(sql_content)\n      schema = {}\n      table_regex = /CREATE TABLE\\s+(?:\"?([\\w.]+)\"?)\\s*\\((.*?)\\);/m\n      sql_content.scan(table_regex) do |table_name, columns_section|\n        schema[normalize_table_name(table_name)] = parse_columns(columns_section)\n      end\n      schema\n    end\n\n    def parse_columns(columns_section)\n      columns = {}\n      columns_section.each_line do |line|\n        line.strip!\n        next if line.empty? || line =~ /^(CONSTRAINT|PRIMARY KEY|FOREIGN KEY)/i\n\n        match = line.match(/\\A\"?(?<col>\\w+)\"?\\s+(?<type>\\w+)(?<size>\\s*\\([\\d,]+\\))?/i)\n        next unless match\n\n        col_name = match[:col]\n        col_type = match[:type].strip.downcase.to_sym\n        options = {}\n        columns[col_name] = { type: col_type, options: options }\n      end\n\n      columns\n    end\n\n    def normalize_table_name(table_name)\n      return table_name unless table_name.include?(\".\")\n\n      table_name.split(\".\").last\n    end\n  end\nend\n"
  },
  {
    "path": "lib/actual_db_schema/version.rb",
    "content": "# frozen_string_literal: true\n\nmodule ActualDbSchema\n  VERSION = \"0.9.1\"\nend\n"
  },
  {
    "path": "lib/actual_db_schema.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"actual_db_schema/engine\"\nrequire \"active_record/migration\"\nrequire \"csv\"\nrequire_relative \"actual_db_schema/git\"\nrequire_relative \"actual_db_schema/rollback_stats_repository\"\nrequire_relative \"actual_db_schema/configuration\"\nrequire_relative \"actual_db_schema/instrumentation\"\nrequire_relative \"actual_db_schema/store\"\nrequire_relative \"actual_db_schema/version\"\nrequire_relative \"actual_db_schema/migration\"\nrequire_relative \"actual_db_schema/failed_migration\"\nrequire_relative \"actual_db_schema/migration_context\"\nrequire_relative \"actual_db_schema/migration_parser\"\nrequire_relative \"actual_db_schema/output_formatter\"\nrequire_relative \"actual_db_schema/patches/migration_proxy\"\nrequire_relative \"actual_db_schema/patches/migrator\"\nrequire_relative \"actual_db_schema/patches/migration_context\"\nrequire_relative \"actual_db_schema/git_hooks\"\nrequire_relative \"actual_db_schema/multi_tenant\"\nrequire_relative \"actual_db_schema/railtie\"\nrequire_relative \"actual_db_schema/schema_diff\"\nrequire_relative \"actual_db_schema/schema_diff_html\"\nrequire_relative \"actual_db_schema/schema_parser\"\nrequire_relative \"actual_db_schema/structure_sql_parser\"\n\nrequire_relative \"actual_db_schema/commands/base\"\nrequire_relative \"actual_db_schema/commands/rollback\"\nrequire_relative \"actual_db_schema/commands/list\"\n\n# The main module definition\nmodule ActualDbSchema\n  raise NotImplementedError, \"ActualDbSchema is only supported in Rails\" unless defined?(Rails)\n\n  class << self\n    attr_accessor :config, :failed\n  end\n\n  self.failed = []\n  self.config = Configuration.new\n\n  def self.configure\n    yield(config)\n  end\n\n  def self.migrated_folder\n    migrated_folders.first\n  end\n\n  def self.migrated_folders\n    return [default_migrated_folder] unless migrations_paths\n\n    Array(migrations_paths).map do |path|\n      if path.end_with?(\"db/migrate\")\n        default_migrated_folder\n      else\n        postfix = path.split(\"/\").last\n        Rails.root.join(\"tmp\", \"migrated_#{postfix}\")\n      end\n    end\n  end\n\n  def self.default_migrated_folder\n    config[:migrated_folder] || Rails.root.join(\"tmp\", \"migrated\")\n  end\n\n  def self.migrations_paths\n    if ActiveRecord::Base.respond_to?(:connection_db_config)\n      ActiveRecord::Base.connection_db_config.migrations_paths\n    else\n      ActiveRecord::Base.connection_config[:migrations_paths]\n    end\n  end\n\n  def self.db_config\n    if ActiveRecord::Base.respond_to?(:connection_db_config)\n      ActiveRecord::Base.connection_db_config.configuration_hash\n    else\n      ActiveRecord::Base.connection_config\n    end\n  end\n\n  def self.migration_filename(fullpath)\n    fullpath.split(\"/\").last\n  end\nend\n\nActiveRecord::MigrationProxy.prepend(ActualDbSchema::Patches::MigrationProxy)\n"
  },
  {
    "path": "lib/generators/actual_db_schema/templates/actual_db_schema.rb",
    "content": "# frozen_string_literal: true\n\n# ActualDbSchema initializer\n# Adjust the configuration as needed.\n\nif defined?(ActualDbSchema)\n  ActualDbSchema.configure do |config|\n    # Enable the gem.\n    config.enabled = Rails.env.development?\n\n    # Disable automatic rollback of phantom migrations.\n    # config.auto_rollback_disabled = true\n    config.auto_rollback_disabled = ENV[\"ACTUAL_DB_SCHEMA_AUTO_ROLLBACK_DISABLED\"].present?\n\n    # Enable the UI for managing migrations.\n    config.ui_enabled = Rails.env.development? || ENV[\"ACTUAL_DB_SCHEMA_UI_ENABLED\"].present?\n\n    # Enable automatic phantom migration rollback on branch switch.\n    # config.git_hooks_enabled = true\n    git_hook_enabled_env = ENV[\"ACTUAL_DB_SCHEMA_GIT_HOOKS_ENABLED\"]\n    config.git_hooks_enabled = git_hook_enabled_env.nil? ? true : git_hook_enabled_env.present?\n\n    # If your application leverages multiple schemas for multi-tenancy, define the active schemas.\n    # config.multi_tenant_schemas = -> { [\"public\", \"tenant1\", \"tenant2\"] }\n\n    # Enable console migrations.\n    # config.console_migrations_enabled = true\n    config.console_migrations_enabled = ENV[\"ACTUAL_DB_SCHEMA_CONSOLE_MIGRATIONS_ENABLED\"].present?\n\n    # Define the migrated folder location.\n    # config.migrated_folder = Rails.root.join(\"custom\", \"migrated\")\n    config.migrated_folder = Rails.root.join(\"tmp\", \"migrated\")\n\n    # Choose where to store migrated files: :file or :db.\n    # config.migrations_storage = :db\n    config.migrations_storage = :file\n  end\n\n  # Subscribe to rollback events to persist stats (optional).\n  # Uncomment the following to track rollback statistics in the database:\n  #\n  # ActiveSupport::Notifications.subscribe(\n  #   ActualDbSchema::Instrumentation::ROLLBACK_EVENT\n  # ) do |_name, _start, _finish, _id, payload|\n  #   ActualDbSchema::RollbackStatsRepository.record(payload)\n  # end\nend\n"
  },
  {
    "path": "lib/tasks/actual_db_schema.rake",
    "content": "# frozen_string_literal: true\n\nnamespace :actual_db_schema do # rubocop:disable Metrics/BlockLength\n  desc \"Install ActualDbSchema initializer and post-checkout git hook.\"\n  task :install do\n    extend ActualDbSchema::OutputFormatter\n\n    initializer_path = Rails.root.join(\"config\", \"initializers\", \"actual_db_schema.rb\")\n    initializer_content = File.read(\n      File.expand_path(\"../../lib/generators/actual_db_schema/templates/actual_db_schema.rb\", __dir__)\n    )\n\n    if File.exist?(initializer_path)\n      puts colorize(\"[ActualDbSchema] An initializer already exists at #{initializer_path}.\", :gray)\n      puts \"Overwrite the existing file at #{initializer_path}? [y,n] \"\n      answer = $stdin.gets.chomp.downcase\n\n      if answer.start_with?(\"y\")\n        File.write(initializer_path, initializer_content)\n        puts colorize(\"[ActualDbSchema] Initializer updated successfully at #{initializer_path}\", :green)\n      else\n        puts colorize(\"[ActualDbSchema] Skipped overwriting the initializer.\", :yellow)\n      end\n    else\n      File.write(initializer_path, initializer_content)\n      puts colorize(\"[ActualDbSchema] Initializer created successfully at #{initializer_path}\", :green)\n    end\n\n    Rake::Task[\"actual_db_schema:install_git_hooks\"].invoke\n  end\n\n  desc \"Install ActualDbSchema post-checkout git hook that rolls back phantom migrations when switching branches.\"\n  task :install_git_hooks do\n    extend ActualDbSchema::OutputFormatter\n\n    puts \"Which Git hook strategy would you like to install? [1, 2, 3]\"\n    puts \"  1) Rollback phantom migrations (db:rollback_branches)\"\n    puts \"  2) Migrate up to latest (db:migrate)\"\n    puts \"  3) No hook installation (skip)\"\n    answer = $stdin.gets.chomp\n\n    strategy =\n      case answer\n      when \"1\" then :rollback\n      when \"2\" then :migrate\n      else\n        :none\n      end\n\n    if strategy == :none\n      puts colorize(\"[ActualDbSchema] Skipping git hook installation.\", :gray)\n    else\n      ActualDbSchema::GitHooks.new(strategy: strategy).install_post_checkout_hook\n    end\n  end\n\n  desc \"Show the schema.rb diff annotated with the migrations that made the changes\"\n  task :diff_schema_with_migrations, %i[schema_path migrations_path] => :environment do |_, args|\n    default_schema = Rails.configuration.active_record.schema_format == :sql ? \"./db/structure.sql\" : \"./db/schema.rb\"\n    schema_path = args[:schema_path] || default_schema\n    migrations_path = args[:migrations_path] || \"db/migrate\"\n\n    schema_diff = ActualDbSchema::SchemaDiff.new(schema_path, migrations_path)\n    puts schema_diff.render\n  end\n\n  desc \"Delete broken migration versions from the database\"\n  task :delete_broken_versions, %i[versions database] => :environment do |_, args|\n    extend ActualDbSchema::OutputFormatter\n\n    if args[:versions]\n      versions = args[:versions].split(\" \").map(&:strip)\n      versions.each do |version|\n        ActualDbSchema::Migration.instance.delete(version, args[:database])\n        puts colorize(\"[ActualDbSchema] Migration #{version} was successfully deleted.\", :green)\n      rescue StandardError => e\n        puts colorize(\"[ActualDbSchema] Error deleting version #{version}: #{e.message}\", :red)\n      end\n    elsif ActualDbSchema::Migration.instance.broken_versions.empty?\n      puts colorize(\"[ActualDbSchema] No broken versions found.\", :gray)\n    else\n      begin\n        ActualDbSchema::Migration.instance.delete_all\n        puts colorize(\"[ActualDbSchema] All broken versions were successfully deleted.\", :green)\n      rescue StandardError => e\n        puts colorize(\"[ActualDbSchema] Error deleting all broken versions: #{e.message}\", :red)\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/tasks/db.rake",
    "content": "# frozen_string_literal: true\n\nnamespace :db do\n  desc \"Rollback migrations that were run inside not a merged branch.\"\n  task rollback_branches: :load_config do\n    ActualDbSchema.failed = []\n    ActualDbSchema::MigrationContext.instance.each do |context|\n      ActualDbSchema::Commands::Rollback.new(context).call\n    end\n  end\n\n  namespace :rollback_branches do\n    desc \"Manually rollback phantom migrations one by one\"\n    task manual: :load_config do\n      ActualDbSchema.failed = []\n      ActualDbSchema::MigrationContext.instance.each do |context|\n        ActualDbSchema::Commands::Rollback.new(context, manual_mode: true).call\n      end\n    end\n  end\n\n  desc \"List all phantom migrations - non-relevant migrations that were run inside not a merged branch.\"\n  task phantom_migrations: :load_config do\n    ActualDbSchema::MigrationContext.instance.each do |context|\n      ActualDbSchema::Commands::List.new(context).call\n    end\n  end\n\n  task \"schema:dump\" => :rollback_branches\nend\n"
  },
  {
    "path": "lib/tasks/test.rake",
    "content": "# frozen_string_literal: true\n\nnamespace :test do # rubocop:disable Metrics/BlockLength\n  desc \"Run tests with SQLite3\"\n  task :sqlite3 do\n    ENV[\"DB_ADAPTER\"] = \"sqlite3\"\n    Rake::Task[\"test\"].invoke\n    Rake::Task[\"test\"].reenable\n  end\n\n  desc \"Run tests with PostgreSQL\"\n  task :postgresql do\n    sh \"docker-compose up -d postgres\"\n    wait_for_postgres\n\n    begin\n      ENV[\"DB_ADAPTER\"] = \"postgresql\"\n      Rake::Task[\"test\"].invoke\n      Rake::Task[\"test\"].reenable\n    ensure\n      sh \"docker-compose down\"\n    end\n  end\n\n  desc \"Run tests with MySQL\"\n  task :mysql2 do\n    sh \"docker-compose up -d mysql\"\n    wait_for_mysql\n\n    begin\n      ENV[\"DB_ADAPTER\"] = \"mysql2\"\n      Rake::Task[\"test\"].invoke\n      Rake::Task[\"test\"].reenable\n    ensure\n      sh \"docker-compose down\"\n    end\n  end\n\n  desc \"Run tests with all adapters (SQLite3, PostgreSQL, MySQL)\"\n  task all: %i[sqlite3 postgresql mysql2]\n\n  def wait_for_postgres\n    retries = 10\n    begin\n      sh \"docker-compose exec -T postgres pg_isready -U postgres\"\n    rescue StandardError\n      retries -= 1\n\n      raise \"PostgreSQL is not ready after several attempts.\" if retries < 1\n\n      sleep 2\n      retry\n    end\n  end\n\n  def wait_for_mysql\n    retries = 10\n    begin\n      sh \"docker-compose exec -T mysql mysqladmin ping -h 127.0.0.1 --silent\"\n    rescue StandardError\n      retries -= 1\n\n      raise \"MySQL is not ready after several attempts.\" if retries < 1\n\n      sleep 2\n      retry\n    end\n  end\nend\n"
  },
  {
    "path": "sig/actual_db_schema.rbs",
    "content": "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",
    "content": "# frozen_string_literal: true\n\nrequire_relative \"../../test_helper\"\nrequire_relative \"../../../app/controllers/actual_db_schema/broken_versions_controller\"\n\nmodule ActualDbSchema\n  class BrokenVersionsControllerDbStorageTest < ActionController::TestCase\n    tests ActualDbSchema::BrokenVersionsController\n\n    def setup\n      setup_utils\n      configure_storage\n      configure_app\n      routes_setup\n      configure_views\n      active_record_setup\n      prepare_database\n    end\n\n    def teardown\n      @utils.clear_db_storage_table(TestingState.db_config)\n      ActualDbSchema.config[:migrations_storage] = :file\n    end\n\n    def routes_setup\n      @routes = @app.routes\n      Rails.application.routes.draw do\n        get \"/rails/broken_versions\" => \"actual_db_schema/broken_versions#index\", as: \"broken_versions\"\n        get \"/rails/migrations\" => \"actual_db_schema/migrations#index\", as: \"migrations\"\n        post \"/rails/broken_version/:id/delete\" => \"actual_db_schema/broken_versions#delete\",\n             as: \"delete_broken_version\"\n        post \"/rails/broken_versions/delete_all\" => \"actual_db_schema/broken_versions#delete_all\",\n             as: \"delete_all_broken_versions\"\n      end\n      ActualDbSchema::BrokenVersionsController.include(@routes.url_helpers)\n    end\n\n    def active_record_setup\n      ActiveRecord::Base.configurations = { \"test\" => TestingState.db_config }\n      ActiveRecord::Tasks::DatabaseTasks.database_configuration = { \"test\" => TestingState.db_config }\n    end\n\n    def setup_utils\n      @utils = TestUtils.new\n    end\n\n    def configure_storage\n      ActualDbSchema.config[:migrations_storage] = :db\n    end\n\n    def configure_app\n      @app = Rails.application\n      Rails.logger = Logger.new($stdout)\n    end\n\n    def configure_views\n      ActionController::Base.view_paths = [File.expand_path(\"../../../app/views/\", __dir__)]\n    end\n\n    def prepare_database\n      @utils.reset_database_yml(TestingState.db_config)\n      @utils.clear_db_storage_table(TestingState.db_config)\n      @utils.cleanup(TestingState.db_config)\n      @utils.prepare_phantom_migrations(TestingState.db_config)\n    end\n\n    def delete_migrations_files\n      delete_primary_migrations\n      delete_secondary_migrations\n    end\n\n    def delete_primary_migrations\n      @utils.delete_migrations_files_for(\"tmp/migrated\")\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"primary\"])\n      [\n        \"tmp/migrated/20130906111511_first_primary.rb\",\n        \"tmp/migrated/20130906111512_second_primary.rb\"\n      ].each do |path|\n        ActualDbSchema::Store.instance.delete(@utils.app_file(path))\n      end\n    end\n\n    def delete_secondary_migrations\n      @utils.delete_migrations_files_for(\"tmp/migrated_migrate_secondary\")\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"secondary\"])\n      [\n        \"tmp/migrated_migrate_secondary/20130906111514_first_secondary.rb\",\n        \"tmp/migrated_migrate_secondary/20130906111515_second_secondary.rb\"\n      ].each do |path|\n        ActualDbSchema::Store.instance.delete(@utils.app_file(path))\n      end\n    end\n\n    test \"GET #index returns a successful response\" do\n      delete_migrations_files\n      get :index\n      assert_response :success\n      assert_select \"table\" do\n        assert_select \"tbody\" do\n          assert_select \"tr\" do\n            assert_select \"td\", text: \"up\"\n            assert_select \"td\", text: \"20130906111511\"\n            assert_select \"td\", text: @utils.branch_for(\"20130906111511\")\n            assert_select \"td\", text: @utils.primary_database\n          end\n          assert_select \"tr\" do\n            assert_select \"td\", text: \"up\"\n            assert_select \"td\", text: \"20130906111512\"\n            assert_select \"td\", text: @utils.branch_for(\"20130906111512\")\n            assert_select \"td\", text: @utils.primary_database\n          end\n          assert_select \"tr\" do\n            assert_select \"td\", text: \"up\"\n            assert_select \"td\", text: \"20130906111514\"\n            assert_select \"td\", text: @utils.branch_for(\"20130906111514\")\n            assert_select \"td\", text: @utils.secondary_database\n          end\n          assert_select \"tr\" do\n            assert_select \"td\", text: \"up\"\n            assert_select \"td\", text: \"20130906111515\"\n            assert_select \"td\", text: @utils.branch_for(\"20130906111515\")\n            assert_select \"td\", text: @utils.secondary_database\n          end\n        end\n      end\n    end\n\n    test \"GET #index when there are no broken versions returns a not found text\" do\n      get :index\n      assert_response :success\n      assert_select \"p\", text: \"No broken versions found.\"\n    end\n\n    test \"POST #delete removes migration entry from the schema_migrations table\" do\n      delete_migrations_files\n      version = \"20130906111511\"\n      sql = \"SELECT version FROM schema_migrations WHERE version = '#{version}'\"\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"primary\"])\n      assert_not_nil ActiveRecord::Base.connection.select_value(sql)\n\n      post :delete, params: { id: \"20130906111511\", database: @utils.primary_database }\n      assert_response :redirect\n      get :index\n      assert_select \"table\" do |table|\n        assert_no_match \"20130906111511\", table.text\n      end\n      assert_select \".flash\", text: \"Migration 20130906111511 was successfully deleted.\"\n      assert_nil ActiveRecord::Base.connection.select_value(sql)\n    end\n\n    test \"POST #delete_all removes all broken migration entries from the schema_migrations table\" do\n      delete_migrations_files\n      sql = \"SELECT COUNT(*) FROM schema_migrations\"\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"primary\"])\n      assert_equal 2, ActiveRecord::Base.connection.select_value(sql)\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"secondary\"])\n      assert_equal 2, ActiveRecord::Base.connection.select_value(sql)\n\n      post :delete_all\n      assert_response :redirect\n      get :index\n      assert_select \"p\", text: \"No broken versions found.\"\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"primary\"])\n      assert_equal 0, ActiveRecord::Base.connection.select_value(sql)\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"secondary\"])\n      assert_equal 0, ActiveRecord::Base.connection.select_value(sql)\n    end\n  end\nend\n"
  },
  {
    "path": "test/controllers/actual_db_schema/broken_versions_controller_test.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative \"../../test_helper\"\nrequire_relative \"../../../app/controllers/actual_db_schema/broken_versions_controller\"\n\nmodule ActualDbSchema\n  class BrokenVersionsControllerTest < ActionController::TestCase\n    def setup\n      @utils = TestUtils.new\n      @app = Rails.application\n      routes_setup\n      Rails.logger = Logger.new($stdout)\n      ActionController::Base.view_paths = [File.expand_path(\"../../../app/views/\", __dir__)]\n      active_record_setup\n      @utils.reset_database_yml(TestingState.db_config)\n      @utils.cleanup(TestingState.db_config)\n      @utils.prepare_phantom_migrations(TestingState.db_config)\n    end\n\n    def routes_setup\n      @routes = @app.routes\n      Rails.application.routes.draw do\n        get \"/rails/broken_versions\" => \"actual_db_schema/broken_versions#index\", as: \"broken_versions\"\n        get \"/rails/migrations\" => \"actual_db_schema/migrations#index\", as: \"migrations\"\n        post \"/rails/broken_version/:id/delete\" => \"actual_db_schema/broken_versions#delete\",\n             as: \"delete_broken_version\"\n        post \"/rails/broken_versions/delete_all\" => \"actual_db_schema/broken_versions#delete_all\",\n             as: \"delete_all_broken_versions\"\n      end\n      ActualDbSchema::BrokenVersionsController.include(@routes.url_helpers)\n    end\n\n    def active_record_setup\n      ActiveRecord::Base.configurations = { \"test\" => TestingState.db_config }\n      ActiveRecord::Tasks::DatabaseTasks.database_configuration = { \"test\" => TestingState.db_config }\n    end\n\n    def delete_migrations_files\n      @utils.delete_migrations_files_for(\"tmp/migrated\")\n      @utils.delete_migrations_files_for(\"tmp/migrated_migrate_secondary\")\n    end\n\n    test \"GET #index returns a successful response\" do\n      delete_migrations_files\n      get :index\n      assert_response :success\n      assert_select \"table\" do\n        assert_select \"tbody\" do\n          assert_select \"tr\" do\n            assert_select \"td\", text: \"up\"\n            assert_select \"td\", text: \"20130906111511\"\n            assert_select \"td\", text: @utils.branch_for(\"20130906111511\")\n            assert_select \"td\", text: @utils.primary_database\n          end\n          assert_select \"tr\" do\n            assert_select \"td\", text: \"up\"\n            assert_select \"td\", text: \"20130906111512\"\n            assert_select \"td\", text: @utils.branch_for(\"20130906111512\")\n            assert_select \"td\", text: @utils.primary_database\n          end\n          assert_select \"tr\" do\n            assert_select \"td\", text: \"up\"\n            assert_select \"td\", text: \"20130906111514\"\n            assert_select \"td\", text: @utils.branch_for(\"20130906111514\")\n            assert_select \"td\", text: @utils.secondary_database\n          end\n          assert_select \"tr\" do\n            assert_select \"td\", text: \"up\"\n            assert_select \"td\", text: \"20130906111515\"\n            assert_select \"td\", text: @utils.branch_for(\"20130906111515\")\n            assert_select \"td\", text: @utils.secondary_database\n          end\n        end\n      end\n    end\n\n    test \"GET #index when there are no broken versions returns a not found text\" do\n      get :index\n      assert_response :success\n      assert_select \"p\", text: \"No broken versions found.\"\n    end\n\n    test \"POST #delete removes migration entry from the schema_migrations table\" do\n      delete_migrations_files\n      version = \"20130906111511\"\n      sql = \"SELECT version FROM schema_migrations WHERE version = '#{version}'\"\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"primary\"])\n      assert_not_nil ActiveRecord::Base.connection.select_value(sql)\n\n      post :delete, params: { id: \"20130906111511\", database: @utils.primary_database }\n      assert_response :redirect\n      get :index\n      assert_select \"table\" do |table|\n        assert_no_match \"20130906111511\", table.text\n      end\n      assert_select \".flash\", text: \"Migration 20130906111511 was successfully deleted.\"\n      assert_nil ActiveRecord::Base.connection.select_value(sql)\n    end\n\n    test \"POST #delete_all removes all broken migration entries from the schema_migrations table\" do\n      delete_migrations_files\n      sql = \"SELECT COUNT(*) FROM schema_migrations\"\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"primary\"])\n      assert_equal 2, ActiveRecord::Base.connection.select_value(sql)\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"secondary\"])\n      assert_equal 2, ActiveRecord::Base.connection.select_value(sql)\n\n      post :delete_all\n      assert_response :redirect\n      get :index\n      assert_select \"p\", text: \"No broken versions found.\"\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"primary\"])\n      assert_equal 0, ActiveRecord::Base.connection.select_value(sql)\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"secondary\"])\n      assert_equal 0, ActiveRecord::Base.connection.select_value(sql)\n    end\n  end\nend\n"
  },
  {
    "path": "test/controllers/actual_db_schema/migrations_controller_db_storage_test.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative \"../../test_helper\"\nrequire_relative \"../../../app/controllers/actual_db_schema/migrations_controller\"\n\nmodule ActualDbSchema\n  class MigrationsControllerDbStorageTest < ActionController::TestCase\n    tests ActualDbSchema::MigrationsController\n\n    def setup\n      setup_utils\n      configure_storage\n      configure_app\n      routes_setup\n      configure_views\n      active_record_setup\n      prepare_database\n    end\n\n    def teardown\n      @utils.clear_db_storage_table(TestingState.db_config)\n      ActualDbSchema.config[:migrations_storage] = :file\n    end\n\n    def routes_setup\n      @routes = @app.routes\n      Rails.application.routes.draw do\n        get \"/rails/phantom_migrations\" => \"actual_db_schema/phantom_migrations#index\", as: \"phantom_migrations\"\n        get \"/rails/broken_versions\" => \"actual_db_schema/broken_versions#index\", as: \"broken_versions\"\n        get \"/rails/schema\" => \"actual_db_schema/schema#index\", as: \"schema\"\n        get \"/rails/migrations\" => \"actual_db_schema/migrations#index\", as: \"migrations\"\n        get \"/rails/migration/:id\" => \"actual_db_schema/migrations#show\", as: \"migration\"\n        post \"/rails/migration/:id/rollback\" => \"actual_db_schema/migrations#rollback\", as: \"rollback_migration\"\n        post \"/rails/migration/:id/migrate\" => \"actual_db_schema/migrations#migrate\", as: \"migrate_migration\"\n      end\n      ActualDbSchema::MigrationsController.include(@routes.url_helpers)\n    end\n\n    def active_record_setup\n      ActiveRecord::Base.configurations = { \"test\" => TestingState.db_config }\n      ActiveRecord::Tasks::DatabaseTasks.database_configuration = { \"test\" => TestingState.db_config }\n    end\n\n    def setup_utils\n      @utils = TestUtils.new\n    end\n\n    def configure_storage\n      ActualDbSchema.config[:migrations_storage] = :db\n    end\n\n    def configure_app\n      @app = Rails.application\n      Rails.logger = Logger.new($stdout)\n    end\n\n    def configure_views\n      ActionController::Base.view_paths = [File.expand_path(\"../../../app/views/\", __dir__)]\n    end\n\n    def prepare_database\n      @utils.reset_database_yml(TestingState.db_config)\n      @utils.clear_db_storage_table(TestingState.db_config)\n      @utils.cleanup(TestingState.db_config)\n      @utils.prepare_phantom_migrations(TestingState.db_config)\n    end\n\n    test \"GET #index returns a successful response\" do\n      get :index\n      assert_response :success\n      assert_select \"table\" do\n        assert_select \"tbody\" do\n          assert_select \"tr\" do\n            assert_select \"td\", text: \"up\"\n            assert_select \"td\", text: \"20130906111511\"\n            assert_select \"td\", text: \"FirstPrimary\"\n            assert_select \"td\", text: @utils.branch_for(\"20130906111511\")\n            assert_select \"td\", text: @utils.primary_database\n          end\n          assert_select \"tr\" do\n            assert_select \"td\", text: \"up\"\n            assert_select \"td\", text: \"20130906111512\"\n            assert_select \"td\", text: \"SecondPrimary\"\n            assert_select \"td\", text: @utils.branch_for(\"20130906111512\")\n            assert_select \"td\", text: @utils.primary_database\n          end\n          assert_select \"tr\" do\n            assert_select \"td\", text: \"up\"\n            assert_select \"td\", text: \"20130906111514\"\n            assert_select \"td\", text: \"FirstSecondary\"\n            assert_select \"td\", text: @utils.branch_for(\"20130906111514\")\n            assert_select \"td\", text: @utils.secondary_database\n          end\n          assert_select \"tr\" do\n            assert_select \"td\", text: \"up\"\n            assert_select \"td\", text: \"20130906111515\"\n            assert_select \"td\", text: \"SecondSecondary\"\n            assert_select \"td\", text: @utils.branch_for(\"20130906111515\")\n            assert_select \"td\", text: @utils.secondary_database\n          end\n        end\n      end\n    end\n\n    test \"GET #index with search query returns filtered results\" do\n      get :index, params: { query: \"primary\" }\n      assert_response :success\n      assert_select \"table\" do\n        assert_select \"tbody\" do\n          assert_select \"tr\" do\n            assert_select \"td\", text: \"up\"\n            assert_select \"td\", text: \"20130906111511\"\n            assert_select \"td\", text: \"FirstPrimary\"\n            assert_select \"td\", text: @utils.branch_for(\"20130906111511\")\n            assert_select \"td\", text: @utils.primary_database\n          end\n          assert_select \"tr\" do\n            assert_select \"td\", text: \"up\"\n            assert_select \"td\", text: \"20130906111512\"\n            assert_select \"td\", text: \"SecondPrimary\"\n            assert_select \"td\", text: @utils.branch_for(\"20130906111512\")\n            assert_select \"td\", text: @utils.primary_database\n          end\n        end\n      end\n\n      assert_no_match \"20130906111514\", @response.body\n      assert_no_match \"20130906111515\", @response.body\n    end\n\n    test \"GET #show returns a successful response\" do\n      get :show, params: { id: \"20130906111511\", database: @utils.primary_database }\n      assert_response :success\n      assert_select \"h2\", text: \"Migration FirstPrimary Details\"\n      assert_select \"table\" do\n        assert_select \"tr\" do\n          assert_select \"th\", text: \"Status\"\n          assert_select \"td\", text: \"up\"\n        end\n        assert_select \"tr\" do\n          assert_select \"th\", text: \"Migration ID\"\n          assert_select \"td\", text: \"20130906111511\"\n        end\n        assert_select \"tr\" do\n          assert_select \"th\", text: \"Database\"\n          assert_select \"td\", text: @utils.primary_database\n        end\n        assert_select \"tr\" do\n          assert_select \"th\", text: \"Branch\"\n          assert_select \"td\", text: @utils.branch_for(\"20130906111511\")\n        end\n      end\n      assert_select \"span.source-badge\", text: \"DB\"\n    end\n\n    test \"GET #show returns a 404 response if migration not found\" do\n      get :show, params: { id: \"nil\", database: @utils.primary_database }\n      assert_response :not_found\n    end\n\n    test \"POST #rollback with irreversible migration returns error message\" do\n      %w[primary secondary].each do |prefix|\n        @utils.define_migration_file(\"20130906111513_irreversible_#{prefix}.rb\", <<~RUBY, prefix: prefix)\n          class Irreversible#{prefix.camelize} < ActiveRecord::Migration[6.0]\n            def up\n              TestingState.up << :irreversible_#{prefix}\n            end\n\n            def down\n              raise ActiveRecord::IrreversibleMigration\n            end\n          end\n        RUBY\n      end\n      @utils.prepare_phantom_migrations(TestingState.db_config)\n      post :rollback, params: { id: \"20130906111513\", database: @utils.primary_database }\n      assert_response :redirect\n      get :index\n      assert_select \".flash\", text: /An error has occurred/\n      assert_select \".flash\", text: /ActiveRecord::IrreversibleMigration/\n    end\n\n    test \"POST #rollback changes migration status to down and hide migration with down status\" do\n      post :rollback, params: { id: \"20130906111511\", database: @utils.primary_database }\n      assert_response :redirect\n      get :index\n      assert_select \"table\" do\n        assert_select \"tbody\" do\n          assert_select \"tr\" do |rows|\n            rows.each do |row|\n              assert_no_match(/down/, row.text)\n            end\n          end\n        end\n      end\n      assert_select \".flash\", text: \"Migration 20130906111511 was successfully rolled back.\"\n    end\n  end\nend\n"
  },
  {
    "path": "test/controllers/actual_db_schema/migrations_controller_test.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative \"../../test_helper\"\nrequire_relative \"../../../app/controllers/actual_db_schema/migrations_controller\"\n\nmodule ActualDbSchema\n  class MigrationsControllerTest < ActionController::TestCase\n    def setup\n      @utils = TestUtils.new\n      @app = Rails.application\n      routes_setup\n      Rails.logger = Logger.new($stdout)\n      ActionController::Base.view_paths = [File.expand_path(\"../../../app/views/\", __dir__)]\n      active_record_setup\n      @utils.reset_database_yml(TestingState.db_config)\n      @utils.cleanup(TestingState.db_config)\n      @utils.prepare_phantom_migrations(TestingState.db_config)\n    end\n\n    def routes_setup\n      @routes = @app.routes\n      Rails.application.routes.draw do\n        get \"/rails/phantom_migrations\" => \"actual_db_schema/phantom_migrations#index\", as: \"phantom_migrations\"\n        get \"/rails/broken_versions\" => \"actual_db_schema/broken_versions#index\", as: \"broken_versions\"\n        get \"/rails/schema\" => \"actual_db_schema/schema#index\", as: \"schema\"\n        get \"/rails/migrations\" => \"actual_db_schema/migrations#index\", as: \"migrations\"\n        get \"/rails/migration/:id\" => \"actual_db_schema/migrations#show\", as: \"migration\"\n        post \"/rails/migration/:id/rollback\" => \"actual_db_schema/migrations#rollback\", as: \"rollback_migration\"\n        post \"/rails/migration/:id/migrate\" => \"actual_db_schema/migrations#migrate\", as: \"migrate_migration\"\n      end\n      ActualDbSchema::MigrationsController.include(@routes.url_helpers)\n    end\n\n    def active_record_setup\n      ActiveRecord::Base.configurations = { \"test\" => TestingState.db_config }\n      ActiveRecord::Tasks::DatabaseTasks.database_configuration = { \"test\" => TestingState.db_config }\n    end\n\n    test \"GET #index returns a successful response\" do\n      get :index\n      assert_response :success\n      assert_select \"table\" do\n        assert_select \"tbody\" do\n          assert_select \"tr\" do\n            assert_select \"td\", text: \"up\"\n            assert_select \"td\", text: \"20130906111511\"\n            assert_select \"td\", text: \"FirstPrimary\"\n            assert_select \"td\", text: @utils.branch_for(\"20130906111511\")\n            assert_select \"td\", text: @utils.primary_database\n          end\n          assert_select \"tr\" do\n            assert_select \"td\", text: \"up\"\n            assert_select \"td\", text: \"20130906111512\"\n            assert_select \"td\", text: \"SecondPrimary\"\n            assert_select \"td\", text: @utils.branch_for(\"20130906111512\")\n            assert_select \"td\", text: @utils.primary_database\n          end\n          assert_select \"tr\" do\n            assert_select \"td\", text: \"up\"\n            assert_select \"td\", text: \"20130906111514\"\n            assert_select \"td\", text: \"FirstSecondary\"\n            assert_select \"td\", text: @utils.branch_for(\"20130906111514\")\n            assert_select \"td\", text: @utils.secondary_database\n          end\n          assert_select \"tr\" do\n            assert_select \"td\", text: \"up\"\n            assert_select \"td\", text: \"20130906111515\"\n            assert_select \"td\", text: \"SecondSecondary\"\n            assert_select \"td\", text: @utils.branch_for(\"20130906111515\")\n            assert_select \"td\", text: @utils.secondary_database\n          end\n        end\n      end\n    end\n\n    test \"GET #index with search query returns filtered results\" do\n      get :index, params: { query: \"primary\" }\n      assert_response :success\n      assert_select \"table\" do\n        assert_select \"tbody\" do\n          assert_select \"tr\" do\n            assert_select \"td\", text: \"up\"\n            assert_select \"td\", text: \"20130906111511\"\n            assert_select \"td\", text: \"FirstPrimary\"\n            assert_select \"td\", text: @utils.branch_for(\"20130906111511\")\n            assert_select \"td\", text: @utils.primary_database\n          end\n          assert_select \"tr\" do\n            assert_select \"td\", text: \"up\"\n            assert_select \"td\", text: \"20130906111512\"\n            assert_select \"td\", text: \"SecondPrimary\"\n            assert_select \"td\", text: @utils.branch_for(\"20130906111512\")\n            assert_select \"td\", text: @utils.primary_database\n          end\n        end\n      end\n\n      assert_no_match \"20130906111514\", @response.body\n      assert_no_match \"20130906111515\", @response.body\n    end\n\n    test \"GET #show returns a successful response\" do\n      get :show, params: { id: \"20130906111511\", database: @utils.primary_database }\n      assert_response :success\n      assert_select \"h2\", text: \"Migration FirstPrimary Details\"\n      assert_select \"table\" do\n        assert_select \"tr\" do\n          assert_select \"th\", text: \"Status\"\n          assert_select \"td\", text: \"up\"\n        end\n        assert_select \"tr\" do\n          assert_select \"th\", text: \"Migration ID\"\n          assert_select \"td\", text: \"20130906111511\"\n        end\n        assert_select \"tr\" do\n          assert_select \"th\", text: \"Database\"\n          assert_select \"td\", text: @utils.primary_database\n        end\n        assert_select \"tr\" do\n          assert_select \"th\", text: \"Branch\"\n          assert_select \"td\", text: @utils.branch_for(\"20130906111511\")\n        end\n      end\n      assert_select \"span.source-badge\", text: \"FILE\"\n    end\n\n    test \"GET #show returns a 404 response if migration not found\" do\n      get :show, params: { id: \"nil\", database: @utils.primary_database }\n      assert_response :not_found\n    end\n\n    test \"POST #rollback with irreversible migration returns error message\" do\n      %w[primary secondary].each do |prefix|\n        @utils.define_migration_file(\"20130906111513_irreversible_#{prefix}.rb\", <<~RUBY, prefix: prefix)\n          class Irreversible#{prefix.camelize} < ActiveRecord::Migration[6.0]\n            def up\n              TestingState.up << :irreversible_#{prefix}\n            end\n\n            def down\n              raise ActiveRecord::IrreversibleMigration\n            end\n          end\n        RUBY\n      end\n      @utils.prepare_phantom_migrations(TestingState.db_config)\n      post :rollback, params: { id: \"20130906111513\", database: @utils.primary_database }\n      assert_response :redirect\n      get :index\n      assert_select \".flash\", text: /An error has occurred/\n      assert_select \".flash\", text: /ActiveRecord::IrreversibleMigration/\n    end\n\n    test \"POST #rollback changes migration status to down and hide migration with down status\" do\n      post :rollback, params: { id: \"20130906111511\", database: @utils.primary_database }\n      assert_response :redirect\n      get :index\n      assert_select \"table\" do\n        assert_select \"tbody\" do\n          assert_select \"tr\" do |rows|\n            rows.each do |row|\n              assert_no_match(/down/, row.text)\n            end\n          end\n        end\n      end\n      assert_select \".flash\", text: \"Migration 20130906111511 was successfully rolled back.\"\n    end\n  end\nend\n"
  },
  {
    "path": "test/controllers/actual_db_schema/phantom_migrations_controller_db_storage_test.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative \"../../test_helper\"\nrequire_relative \"../../../app/controllers/actual_db_schema/phantom_migrations_controller\"\n\nmodule ActualDbSchema\n  class PhantomMigrationsControllerDbStorageTest < ActionController::TestCase\n    tests ActualDbSchema::PhantomMigrationsController\n\n    def setup\n      setup_utils\n      configure_storage\n      configure_app\n      routes_setup\n      configure_views\n      active_record_setup\n      prepare_database\n    end\n\n    def teardown\n      @utils.clear_db_storage_table(TestingState.db_config)\n      ActualDbSchema.config[:migrations_storage] = :file\n    end\n\n    def routes_setup\n      @routes = @app.routes\n      Rails.application.routes.draw do\n        get \"/rails/migrations\" => \"actual_db_schema/migrations#index\", as: \"migrations\"\n        get \"/rails/phantom_migrations\" => \"actual_db_schema/phantom_migrations#index\", as: \"phantom_migrations\"\n        get \"/rails/phantom_migration/:id\" => \"actual_db_schema/phantom_migrations#show\", as: \"phantom_migration\"\n        post \"/rails/phantom_migration/:id/rollback\" => \"actual_db_schema/phantom_migrations#rollback\",\n             as: \"rollback_phantom_migration\"\n        post \"/rails/phantom_migrations/rollback_all\" => \"actual_db_schema/phantom_migrations#rollback_all\",\n             as: \"rollback_all_phantom_migrations\"\n      end\n      ActualDbSchema::PhantomMigrationsController.include(@routes.url_helpers)\n    end\n\n    def active_record_setup\n      ActiveRecord::Base.configurations = { \"test\" => TestingState.db_config }\n      ActiveRecord::Tasks::DatabaseTasks.database_configuration = { \"test\" => TestingState.db_config }\n    end\n\n    def setup_utils\n      @utils = TestUtils.new\n    end\n\n    def configure_storage\n      ActualDbSchema.config[:migrations_storage] = :db\n    end\n\n    def configure_app\n      @app = Rails.application\n      Rails.logger = Logger.new($stdout)\n    end\n\n    def configure_views\n      ActionController::Base.view_paths = [File.expand_path(\"../../../app/views/\", __dir__)]\n    end\n\n    def prepare_database\n      @utils.reset_database_yml(TestingState.db_config)\n      @utils.clear_db_storage_table(TestingState.db_config)\n      @utils.cleanup(TestingState.db_config)\n      @utils.prepare_phantom_migrations(TestingState.db_config)\n    end\n\n    test \"GET #index returns a successful response\" do\n      get :index\n      assert_response :success\n      assert_select \"table\" do\n        assert_select \"tbody\" do\n          assert_select \"tr\" do |rows|\n            rows.each do |row|\n              assert_no_match(/down/, row.text)\n            end\n          end\n          assert_select \"tr\" do\n            assert_select \"td\", text: \"up\"\n            assert_select \"td\", text: \"20130906111511\"\n            assert_select \"td\", text: \"FirstPrimary\"\n            assert_select \"td\", text: @utils.branch_for(\"20130906111511\")\n            assert_select \"td\", text: @utils.primary_database\n          end\n          assert_select \"tr\" do\n            assert_select \"td\", text: \"up\"\n            assert_select \"td\", text: \"20130906111512\"\n            assert_select \"td\", text: \"SecondPrimary\"\n            assert_select \"td\", text: @utils.branch_for(\"20130906111512\")\n            assert_select \"td\", text: @utils.primary_database\n          end\n          assert_select \"tr\" do\n            assert_select \"td\", text: \"up\"\n            assert_select \"td\", text: \"20130906111514\"\n            assert_select \"td\", text: \"FirstSecondary\"\n            assert_select \"td\", text: @utils.branch_for(\"20130906111514\")\n            assert_select \"td\", text: @utils.secondary_database\n          end\n          assert_select \"tr\" do\n            assert_select \"td\", text: \"up\"\n            assert_select \"td\", text: \"20130906111515\"\n            assert_select \"td\", text: \"SecondSecondary\"\n            assert_select \"td\", text: @utils.branch_for(\"20130906111515\")\n            assert_select \"td\", text: @utils.secondary_database\n          end\n        end\n      end\n    end\n\n    test \"GET #index when all migrations is down returns a not found text\" do\n      @utils.run_migrations\n      get :index\n      assert_response :success\n      assert_select \"p\", text: \"No phantom migrations found.\"\n    end\n\n    test \"GET #show returns a successful response\" do\n      get :show, params: { id: \"20130906111511\", database: @utils.primary_database }\n      assert_response :success\n      assert_select \"h2\", text: \"Phantom Migration FirstPrimary Details\"\n      assert_select \"table\" do\n        assert_select \"tr\" do\n          assert_select \"th\", text: \"Status\"\n          assert_select \"td\", text: \"up\"\n        end\n        assert_select \"tr\" do\n          assert_select \"th\", text: \"Migration ID\"\n          assert_select \"td\", text: \"20130906111511\"\n        end\n        assert_select \"tr\" do\n          assert_select \"th\", text: \"Database\"\n          assert_select \"td\", text: @utils.primary_database\n        end\n        assert_select \"tr\" do\n          assert_select \"th\", text: \"Branch\"\n          assert_select \"td\", text: @utils.branch_for(\"20130906111511\")\n        end\n      end\n      assert_select \"span.source-badge\", text: \"DB\"\n    end\n\n    test \"GET #show returns a 404 response if migration not found\" do\n      get :show, params: { id: \"nil\", database: @utils.primary_database }\n      assert_response :not_found\n    end\n\n    test \"POST #rollback changes migration status to down and hide migration with down status\" do\n      post :rollback, params: { id: \"20130906111511\", database: @utils.primary_database }\n      assert_response :redirect\n      get :index\n      assert_select \"table\" do\n        assert_select \"tbody\" do\n          assert_select \"tr\" do |rows|\n            rows.each do |row|\n              assert_no_match(/down/, row.text)\n            end\n          end\n          assert_select \"tr\" do\n            assert_select \"td\", text: \"up\"\n            assert_select \"td\", text: \"20130906111512\"\n            assert_select \"td\", text: \"SecondPrimary\"\n            assert_select \"td\", text: @utils.branch_for(\"20130906111512\")\n          end\n        end\n      end\n      assert_select \".flash\", text: \"Migration 20130906111511 was successfully rolled back.\"\n    end\n\n    test \"POST #rollback with irreversible migration returns error message\" do\n      %w[primary secondary].each do |prefix|\n        @utils.define_migration_file(\"20130906111513_irreversible_#{prefix}.rb\", <<~RUBY, prefix: prefix)\n          class Irreversible#{prefix.camelize} < ActiveRecord::Migration[6.0]\n            def up\n              TestingState.up << :irreversible_#{prefix}\n            end\n\n            def down\n              raise ActiveRecord::IrreversibleMigration\n            end\n          end\n        RUBY\n      end\n      @utils.prepare_phantom_migrations(TestingState.db_config)\n      post :rollback, params: { id: \"20130906111513\", database: @utils.primary_database }\n      assert_response :redirect\n      get :index\n      assert_select \".flash\", text: /An error has occurred/\n      assert_select \".flash\", text: /ActiveRecord::IrreversibleMigration/\n    end\n\n    test \"POST #rollback_all changes all phantom migrations status to down and hide migration with down status\" do\n      post :rollback_all\n      assert_response :redirect\n      get :index\n      assert_select \"p\", text: \"No phantom migrations found.\"\n    end\n  end\nend\n"
  },
  {
    "path": "test/controllers/actual_db_schema/phantom_migrations_controller_test.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative \"../../test_helper\"\nrequire_relative \"../../../app/controllers/actual_db_schema/phantom_migrations_controller\"\n\nmodule ActualDbSchema\n  class PhantomMigrationsControllerTest < ActionController::TestCase\n    def setup\n      @utils = TestUtils.new\n      @app = Rails.application\n      routes_setup\n      Rails.logger = Logger.new($stdout)\n      ActionController::Base.view_paths = [File.expand_path(\"../../../app/views/\", __dir__)]\n      active_record_setup\n      @utils.reset_database_yml(TestingState.db_config)\n      @utils.cleanup(TestingState.db_config)\n      @utils.prepare_phantom_migrations(TestingState.db_config)\n    end\n\n    def routes_setup\n      @routes = @app.routes\n      Rails.application.routes.draw do\n        get \"/rails/migrations\" => \"actual_db_schema/migrations#index\", as: \"migrations\"\n        get \"/rails/phantom_migrations\" => \"actual_db_schema/phantom_migrations#index\", as: \"phantom_migrations\"\n        get \"/rails/phantom_migration/:id\" => \"actual_db_schema/phantom_migrations#show\", as: \"phantom_migration\"\n        post \"/rails/phantom_migration/:id/rollback\" => \"actual_db_schema/phantom_migrations#rollback\",\n             as: \"rollback_phantom_migration\"\n        post \"/rails/phantom_migrations/rollback_all\" => \"actual_db_schema/phantom_migrations#rollback_all\",\n             as: \"rollback_all_phantom_migrations\"\n      end\n      ActualDbSchema::PhantomMigrationsController.include(@routes.url_helpers)\n    end\n\n    def active_record_setup\n      ActiveRecord::Base.configurations = { \"test\" => TestingState.db_config }\n      ActiveRecord::Tasks::DatabaseTasks.database_configuration = { \"test\" => TestingState.db_config }\n    end\n\n    test \"GET #index returns a successful response\" do\n      get :index\n      assert_response :success\n      assert_select \"table\" do\n        assert_select \"tbody\" do\n          assert_select \"tr\" do |rows|\n            rows.each do |row|\n              assert_no_match(/down/, row.text)\n            end\n          end\n          assert_select \"tr\" do\n            assert_select \"td\", text: \"up\"\n            assert_select \"td\", text: \"20130906111511\"\n            assert_select \"td\", text: \"FirstPrimary\"\n            assert_select \"td\", text: @utils.branch_for(\"20130906111511\")\n            assert_select \"td\", text: @utils.primary_database\n          end\n          assert_select \"tr\" do\n            assert_select \"td\", text: \"up\"\n            assert_select \"td\", text: \"20130906111512\"\n            assert_select \"td\", text: \"SecondPrimary\"\n            assert_select \"td\", text: @utils.branch_for(\"20130906111512\")\n            assert_select \"td\", text: @utils.primary_database\n          end\n          assert_select \"tr\" do\n            assert_select \"td\", text: \"up\"\n            assert_select \"td\", text: \"20130906111514\"\n            assert_select \"td\", text: \"FirstSecondary\"\n            assert_select \"td\", text: @utils.branch_for(\"20130906111514\")\n            assert_select \"td\", text: @utils.secondary_database\n          end\n          assert_select \"tr\" do\n            assert_select \"td\", text: \"up\"\n            assert_select \"td\", text: \"20130906111515\"\n            assert_select \"td\", text: \"SecondSecondary\"\n            assert_select \"td\", text: @utils.branch_for(\"20130906111515\")\n            assert_select \"td\", text: @utils.secondary_database\n          end\n        end\n      end\n    end\n\n    test \"GET #index when all migrations is down returns a not found text\" do\n      @utils.run_migrations\n      get :index\n      assert_response :success\n      assert_select \"p\", text: \"No phantom migrations found.\"\n    end\n\n    test \"GET #show returns a successful response\" do\n      get :show, params: { id: \"20130906111511\", database: @utils.primary_database }\n      assert_response :success\n      assert_select \"h2\", text: \"Phantom Migration FirstPrimary Details\"\n      assert_select \"table\" do\n        assert_select \"tr\" do\n          assert_select \"th\", text: \"Status\"\n          assert_select \"td\", text: \"up\"\n        end\n        assert_select \"tr\" do\n          assert_select \"th\", text: \"Migration ID\"\n          assert_select \"td\", text: \"20130906111511\"\n        end\n        assert_select \"tr\" do\n          assert_select \"th\", text: \"Database\"\n          assert_select \"td\", text: @utils.primary_database\n        end\n        assert_select \"tr\" do\n          assert_select \"th\", text: \"Branch\"\n          assert_select \"td\", text: @utils.branch_for(\"20130906111511\")\n        end\n      end\n      assert_select \"span.source-badge\", text: \"FILE\"\n    end\n\n    test \"GET #show returns a 404 response if migration not found\" do\n      get :show, params: { id: \"nil\", database: @utils.primary_database }\n      assert_response :not_found\n    end\n\n    test \"POST #rollback changes migration status to down and hide migration with down status\" do\n      post :rollback, params: { id: \"20130906111511\", database: @utils.primary_database }\n      assert_response :redirect\n      get :index\n      assert_select \"table\" do\n        assert_select \"tbody\" do\n          assert_select \"tr\" do |rows|\n            rows.each do |row|\n              assert_no_match(/down/, row.text)\n            end\n          end\n          assert_select \"tr\" do\n            assert_select \"td\", text: \"up\"\n            assert_select \"td\", text: \"20130906111512\"\n            assert_select \"td\", text: \"SecondPrimary\"\n            assert_select \"td\", text: @utils.branch_for(\"20130906111512\")\n          end\n        end\n      end\n      assert_select \".flash\", text: \"Migration 20130906111511 was successfully rolled back.\"\n    end\n\n    test \"POST #rollback with irreversible migration returns error message\" do\n      %w[primary secondary].each do |prefix|\n        @utils.define_migration_file(\"20130906111513_irreversible_#{prefix}.rb\", <<~RUBY, prefix: prefix)\n          class Irreversible#{prefix.camelize} < ActiveRecord::Migration[6.0]\n            def up\n              TestingState.up << :irreversible_#{prefix}\n            end\n\n            def down\n              raise ActiveRecord::IrreversibleMigration\n            end\n          end\n        RUBY\n      end\n      @utils.prepare_phantom_migrations(TestingState.db_config)\n      post :rollback, params: { id: \"20130906111513\", database: @utils.primary_database }\n      assert_response :redirect\n      get :index\n      assert_select \".flash\", text: /An error has occurred/\n      assert_select \".flash\", text: /ActiveRecord::IrreversibleMigration/\n    end\n\n    test \"POST #rollback_all changes all phantom migrations status to down and hide migration with down status\" do\n      post :rollback_all\n      assert_response :redirect\n      get :index\n      assert_select \"p\", text: \"No phantom migrations found.\"\n    end\n  end\nend\n"
  },
  {
    "path": "test/controllers/actual_db_schema/schema_controller_db_storage_test.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative \"../../test_helper\"\nrequire_relative \"../../../app/controllers/actual_db_schema/schema_controller\"\n\nmodule ActualDbSchema\n  class SchemaControllerDbStorageTest < ActionController::TestCase\n    tests ActualDbSchema::SchemaController\n\n    def setup\n      setup_utils\n      configure_storage\n      configure_app\n      routes_setup\n      configure_views\n      active_record_setup\n      prepare_database\n      stub_schema_diff\n    end\n\n    def teardown\n      @utils.define_migration_file(\"20250212084323_drop_users.rb\", <<~RUBY)\n        class DropUsers < ActiveRecord::Migration[6.0]\n          def change\n            drop_table :users, if_exists: true\n          end\n        end\n      RUBY\n      @utils.define_migration_file(\"20250212084324_drop_products.rb\", <<~RUBY)\n        class DropProducts < ActiveRecord::Migration[6.0]\n          def change\n            drop_table :products, if_exists: true\n          end\n        end\n      RUBY\n      @utils.run_migrations\n      @utils.clear_db_storage_table(TestingState.db_config)\n      ActualDbSchema.config[:migrations_storage] = :file\n    end\n\n    def routes_setup\n      @routes = @app.routes\n      Rails.application.routes.draw do\n        get \"/rails/migrations\" => \"actual_db_schema/migrations#index\", as: \"migrations\"\n        get \"/rails/schema\" => \"actual_db_schema/schema#index\", as: \"schema\"\n      end\n      ActualDbSchema::SchemaController.include(@routes.url_helpers)\n    end\n\n    def active_record_setup\n      ActiveRecord::Base.configurations = { \"test\" => TestingState.db_config }\n      ActiveRecord::Tasks::DatabaseTasks.database_configuration = { \"test\" => TestingState.db_config }\n    end\n\n    def setup_utils\n      @utils = TestUtils.new\n    end\n\n    def configure_storage\n      ActualDbSchema.config[:migrations_storage] = :db\n    end\n\n    def configure_app\n      @app = Rails.application\n      Rails.logger = Logger.new($stdout)\n    end\n\n    def configure_views\n      ActionController::Base.view_paths = [File.expand_path(\"../../../app/views/\", __dir__)]\n    end\n\n    def prepare_database\n      @utils.reset_database_yml(TestingState.db_config)\n      @utils.clear_db_storage_table(TestingState.db_config)\n      @utils.cleanup(TestingState.db_config)\n      define_migrations\n    end\n\n    def stub_schema_diff\n      ActualDbSchema::SchemaDiffHtml.define_method(:initialize) do |_schema_path, _migrations_path|\n        @schema_path = \"test/dummy_app/db/schema.rb\"\n        @migrations_path = \"test/dummy_app/db/migrate\"\n      end\n    end\n\n    def define_migrations\n      @utils.define_migration_file(\"20250212084321_create_users_table.rb\", <<~RUBY)\n        class CreateUsersTable < ActiveRecord::Migration[6.0]\n          def change\n            create_table :users do |t|\n              t.string :name\n              t.timestamps\n            end\n          end\n        end\n      RUBY\n      @utils.define_migration_file(\"20250212084322_create_products_table.rb\", <<~RUBY)\n        class CreateProductsTable < ActiveRecord::Migration[6.0]\n          def change\n            create_table :products do |t|\n              t.string :name\n              t.timestamps\n            end\n          end\n        end\n      RUBY\n      @utils.run_migrations\n\n      ActualDbSchema::SchemaDiff.define_method(:old_schema_content) do\n        <<~RUBY\n          ActiveRecord::Schema[6.0].define(version: 20250212084322) do\n            create_table \"products\", force: :cascade do |t|\n              t.string \"name\"\n              t.datetime \"created_at\", null: false\n              t.datetime \"updated_at\", null: false\n            end\n\n            create_table \"users\", force: :cascade do |t|\n              t.string \"name\"\n              t.datetime \"created_at\", null: false\n              t.datetime \"updated_at\", null: false\n            end\n          end\n        RUBY\n      end\n    end\n\n    test \"GET #index returns a successful response\" do\n      file_name = \"20250212084325_add_surname_to_users.rb\"\n      @utils.define_migration_file(file_name, <<~RUBY)\n        class AddSurnameToUsers < ActiveRecord::Migration[6.0]\n          def change\n            add_column :users, :surname, :string\n          end\n        end\n      RUBY\n      @utils.run_migrations\n\n      get :index\n      assert_response :success\n      assert_select \"h2\", text: \"Database Schema\"\n      assert_select \"div.schema-diff pre\" do |pre|\n        assert_match(/create_table \"products\"/, pre.text)\n        assert_match(/create_table \"users\"/, pre.text)\n        assert_match(%r{\\+    t\\.string \"surname\" // #{File.join(\"test/dummy_app/db/migrate\", file_name)} //}, pre.text)\n      end\n    end\n\n    test \"GET #index with search query returns filtered results\" do\n      get :index, params: { table: \"users\" }\n      assert_response :success\n      assert_select \"h2\", text: \"Database Schema\"\n      assert_select \"div.schema-diff pre\" do |pre|\n        assert_match(/create_table \"users\"/, pre.text)\n        refute_match(/create_table \"products\"/, pre.text)\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "test/controllers/actual_db_schema/schema_controller_test.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative \"../../test_helper\"\nrequire_relative \"../../../app/controllers/actual_db_schema/schema_controller\"\n\nmodule ActualDbSchema\n  class SchemaControllerTest < ActionController::TestCase\n    def setup\n      @utils = TestUtils.new\n      @app = Rails.application\n      routes_setup\n      Rails.logger = Logger.new($stdout)\n      ActionController::Base.view_paths = [File.expand_path(\"../../../app/views/\", __dir__)]\n      active_record_setup\n      @utils.reset_database_yml(TestingState.db_config)\n      @utils.cleanup(TestingState.db_config)\n      define_migrations\n    end\n\n    def teardown\n      @utils.define_migration_file(\"20250212084323_drop_users_table.rb\", <<~RUBY)\n        class DropUsersTable < ActiveRecord::Migration[6.0]\n          def change\n            drop_table :users, if_exists: true\n          end\n        end\n      RUBY\n      @utils.define_migration_file(\"20250212084324_drop_products_table.rb\", <<~RUBY)\n        class DropProductsTable < ActiveRecord::Migration[6.0]\n          def change\n            drop_table :products, if_exists: true\n          end\n        end\n      RUBY\n      @utils.run_migrations\n    end\n\n    def routes_setup\n      @routes = @app.routes\n      Rails.application.routes.draw do\n        get \"/rails/migrations\" => \"actual_db_schema/migrations#index\", as: \"migrations\"\n        get \"/rails/schema\" => \"actual_db_schema/schema#index\", as: \"schema\"\n      end\n      ActualDbSchema::SchemaController.include(@routes.url_helpers)\n    end\n\n    def active_record_setup\n      ActiveRecord::Base.configurations = { \"test\" => TestingState.db_config }\n      ActiveRecord::Tasks::DatabaseTasks.database_configuration = { \"test\" => TestingState.db_config }\n    end\n\n    def define_migrations\n      @utils.define_migration_file(\"20250212084321_create_users_table.rb\", <<~RUBY)\n        class CreateUsersTable < ActiveRecord::Migration[6.0]\n          def change\n            create_table :users do |t|\n              t.string :name\n              t.timestamps\n            end\n          end\n        end\n      RUBY\n      @utils.define_migration_file(\"20250212084322_create_products_table.rb\", <<~RUBY)\n        class CreateProductsTable < ActiveRecord::Migration[6.0]\n          def change\n            create_table :products do |t|\n              t.string :name\n              t.timestamps\n            end\n          end\n        end\n      RUBY\n      @utils.run_migrations\n    end\n\n    def run_migration(file_name, content)\n      @utils.define_migration_file(file_name, content)\n      @utils.run_migrations\n      dump_schema\n    end\n\n    def dump_schema\n      return unless Rails.configuration.active_record.schema_format == :sql\n\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"primary\"])\n      config = if ActiveRecord::Base.respond_to?(:connection_db_config)\n                 ActiveRecord::Base.connection_db_config\n               else\n                 ActiveRecord::Base.configurations[Rails.env]\n               end\n      ActiveRecord::Tasks::DatabaseTasks.structure_dump(config, Rails.root.join(\"db\", \"structure.sql\").to_s)\n    end\n\n    def define_schema_diff_html_methods_for_schema_rb\n      old_schema_content = File.read(\"test/dummy_app/db/schema.rb\")\n      ActualDbSchema::SchemaDiff.define_method(:old_schema_content) { old_schema_content }\n      ActualDbSchema::SchemaDiffHtml.define_method(:initialize) do |_schema_path, _migrations_path|\n        @schema_path = \"test/dummy_app/db/schema.rb\"\n        @migrations_path = \"test/dummy_app/db/migrate\"\n      end\n    end\n\n    def define_schema_diff_html_methods_for_structure_sql\n      old_schema_content = File.read(\"test/dummy_app/db/structure.sql\")\n      ActualDbSchema::SchemaDiff.define_method(:old_schema_content) { old_schema_content }\n      ActualDbSchema::SchemaDiffHtml.define_method(:initialize) do |_schema_path, _migrations_path|\n        @schema_path = \"test/dummy_app/db/structure.sql\"\n        @migrations_path = \"test/dummy_app/db/migrate\"\n      end\n    end\n\n    def add_surname_to_users_migration\n      <<~RUBY\n        class AddSurnameToUsers < ActiveRecord::Migration[6.0]\n          def change\n            add_column :users, :surname, :string\n          end\n        end\n      RUBY\n    end\n\n    test \"GET #index returns a successful response when using schema.rb\" do\n      define_schema_diff_html_methods_for_schema_rb\n      file_name = \"20250212084325_add_surname_to_users.rb\"\n      run_migration(file_name, add_surname_to_users_migration)\n      get :index\n      assert_response :success\n      assert_select \"h2\", text: \"Database Schema\"\n      assert_select \"div.schema-diff pre\" do |pre|\n        assert_match(/create_table \"products\"/, pre.text)\n        assert_match(/create_table \"users\"/, pre.text)\n        assert_match(%r{\\+    t\\.string \"surname\" // #{File.join(\"test/dummy_app/db/migrate\", file_name)} //}, pre.text)\n      end\n    end\n\n    test \"GET #index with search query returns filtered results when using schema.rb\" do\n      define_schema_diff_html_methods_for_schema_rb\n      get :index, params: { table: \"users\" }\n      assert_response :success\n      assert_select \"h2\", text: \"Database Schema\"\n      assert_select \"div.schema-diff pre\" do |pre|\n        assert_match(/create_table \"users\"/, pre.text)\n        refute_match(/create_table \"products\"/, pre.text)\n      end\n    end\n\n    test \"GET #index returns a successful response when using structure.sql\" do\n      skip unless TestingState.db_config[\"primary\"][\"adapter\"] == \"postgresql\"\n\n      Rails.application.configure { config.active_record.schema_format = :sql }\n      dump_schema\n      define_schema_diff_html_methods_for_structure_sql\n      file_name = \"20250212084325_add_surname_to_users.rb\"\n      run_migration(file_name, add_surname_to_users_migration)\n      get :index\n      assert_response :success\n      assert_select \"h2\", text: \"Database Schema\"\n      assert_select \"div.schema-diff pre\" do |pre|\n        assert_match(/CREATE TABLE public.products/, pre.text)\n        assert_match(/CREATE TABLE public.users/, pre.text)\n        assert_match(\n          %r{\\+    surname character varying // #{File.join(\"test/dummy_app/db/migrate\", file_name)} //}, pre.text\n        )\n      end\n    end\n\n    test \"GET #index with search query returns filtered results when using structure.sql\" do\n      skip unless TestingState.db_config[\"primary\"][\"adapter\"] == \"postgresql\"\n\n      Rails.application.configure { config.active_record.schema_format = :sql }\n      dump_schema\n      define_schema_diff_html_methods_for_structure_sql\n      get :index, params: { table: \"users\" }\n      assert_response :success\n      assert_select \"h2\", text: \"Database Schema\"\n      assert_select \"div.schema-diff pre\" do |pre|\n        assert_match(/CREATE TABLE public.users/, pre.text)\n        refute_match(/CREATE TABLE public.products/, pre.text)\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "test/dummy_app/config/.keep",
    "content": "\n"
  },
  {
    "path": "test/dummy_app/db/migrate/.keep",
    "content": ""
  },
  {
    "path": "test/dummy_app/db/migrate_secondary/.keep",
    "content": ""
  },
  {
    "path": "test/dummy_app/public/404.html",
    "content": ""
  },
  {
    "path": "test/rake_task_console_migrations_db_storage_test.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"test_helper\"\nrequire_relative \"../lib/actual_db_schema/console_migrations\"\n\ndescribe \"console migrations (db storage)\" do\n  let(:utils) { TestUtils.new }\n\n  before do\n    ActualDbSchema.config[:migrations_storage] = :db\n    utils.clear_db_storage_table\n    extend ActualDbSchema::ConsoleMigrations\n\n    utils.reset_database_yml(TestingState.db_config[\"primary\"])\n    ActiveRecord::Base.configurations = { \"test\" => TestingState.db_config[\"primary\"] }\n    ActiveRecord::Tasks::DatabaseTasks.database_configuration = { \"test\" => TestingState.db_config[\"primary\"] }\n    ActiveRecord::Base.establish_connection(**TestingState.db_config[\"primary\"])\n    utils.cleanup\n\n    utils.define_migration_file(\"20250124084321_create_users.rb\", <<~RUBY)\n      class CreateUsers < ActiveRecord::Migration[6.0]\n        def change\n          create_table :users do |t|\n            t.string :name\n            t.string :middle_name\n            t.timestamps\n          end\n\n          add_index :users, :name, name: \"index_users_on_name\", unique: true\n        end\n      end\n    RUBY\n    utils.run_migrations\n  end\n\n  after do\n    utils.define_migration_file(\"20250124084323_drop_users.rb\", <<~RUBY)\n      class DropUsers < ActiveRecord::Migration[6.0]\n        def change\n          drop_table :users\n        end\n      end\n    RUBY\n    utils.run_migrations\n    utils.clear_db_storage_table\n    ActualDbSchema.config[:migrations_storage] = :file\n  end\n\n  it \"adds a column to a table\" do\n    add_column :users, :email, :string\n    assert ActiveRecord::Base.connection.column_exists?(:users, :email)\n  end\n\n  it \"removes a column from a table\" do\n    remove_column :users, :middle_name\n    refute ActiveRecord::Base.connection.column_exists?(:users, :middle_name)\n  end\n\n  it \"creates and drops a table\" do\n    refute ActiveRecord::Base.connection.table_exists?(:categories)\n    create_table :categories do |t|\n      t.string :title\n      t.timestamps\n    end\n    assert ActiveRecord::Base.connection.table_exists?(:categories)\n\n    drop_table :categories\n    refute ActiveRecord::Base.connection.table_exists?(:categories)\n  end\n\n  it \"changes column type\" do\n    change_column :users, :middle_name, :text\n    assert_equal :text, ActiveRecord::Base.connection.columns(:users).find { |c| c.name == \"middle_name\" }.type\n  end\n\n  it \"renames a column\" do\n    rename_column :users, :name, :full_name\n    assert ActiveRecord::Base.connection.column_exists?(:users, :full_name)\n    refute ActiveRecord::Base.connection.column_exists?(:users, :name)\n  end\n\n  it \"adds and removes an index\" do\n    add_index :users, :middle_name, name: \"index_users_on_middle_name\", unique: true\n    assert ActiveRecord::Base.connection.index_exists?(:users, :middle_name, name: \"index_users_on_middle_name\")\n\n    remove_index :users, name: \"index_users_on_middle_name\"\n    refute ActiveRecord::Base.connection.index_exists?(:users, :middle_name, name: \"index_users_on_middle_name\")\n  end\n\n  it \"adds and removes timestamps\" do\n    remove_timestamps :users\n    refute ActiveRecord::Base.connection.column_exists?(:users, :created_at)\n    refute ActiveRecord::Base.connection.column_exists?(:users, :updated_at)\n\n    add_timestamps :users\n    assert ActiveRecord::Base.connection.column_exists?(:users, :created_at)\n    assert ActiveRecord::Base.connection.column_exists?(:users, :updated_at)\n  end\nend\n"
  },
  {
    "path": "test/rake_task_console_migrations_test.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"test_helper\"\nrequire_relative \"../lib/actual_db_schema/console_migrations\"\n\ndescribe \"console migrations\" do\n  let(:utils) { TestUtils.new }\n\n  before do\n    extend ActualDbSchema::ConsoleMigrations\n\n    utils.reset_database_yml(TestingState.db_config[\"primary\"])\n    ActiveRecord::Base.configurations = { \"test\" => TestingState.db_config[\"primary\"] }\n    ActiveRecord::Tasks::DatabaseTasks.database_configuration = { \"test\" => TestingState.db_config[\"primary\"] }\n    ActiveRecord::Base.establish_connection(**TestingState.db_config[\"primary\"])\n    utils.cleanup\n\n    utils.define_migration_file(\"20250124084321_create_users.rb\", <<~RUBY)\n      class CreateUsers < ActiveRecord::Migration[6.0]\n        def change\n          create_table :users do |t|\n            t.string :name\n            t.string :middle_name\n            t.timestamps\n          end\n\n          add_index :users, :name, name: \"index_users_on_name\", unique: true\n        end\n      end\n    RUBY\n    utils.run_migrations\n  end\n\n  after do\n    utils.define_migration_file(\"20250124084323_drop_users.rb\", <<~RUBY)\n      class DropUsers < ActiveRecord::Migration[6.0]\n        def change\n          drop_table :users\n        end\n      end\n    RUBY\n    utils.run_migrations\n  end\n\n  it \"adds a column to a table\" do\n    add_column :users, :email, :string\n    assert ActiveRecord::Base.connection.column_exists?(:users, :email)\n  end\n\n  it \"removes a column from a table\" do\n    remove_column :users, :middle_name\n    refute ActiveRecord::Base.connection.column_exists?(:users, :middle_name)\n  end\n\n  it \"creates and drops a table\" do\n    refute ActiveRecord::Base.connection.table_exists?(:categories)\n    create_table :categories do |t|\n      t.string :title\n      t.timestamps\n    end\n    assert ActiveRecord::Base.connection.table_exists?(:categories)\n\n    drop_table :categories\n    refute ActiveRecord::Base.connection.table_exists?(:categories)\n  end\n\n  it \"changes column type\" do\n    change_column :users, :middle_name, :text\n    assert_equal :text, ActiveRecord::Base.connection.columns(:users).find { |c| c.name == \"middle_name\" }.type\n  end\n\n  it \"renames a column\" do\n    rename_column :users, :name, :full_name\n    assert ActiveRecord::Base.connection.column_exists?(:users, :full_name)\n    refute ActiveRecord::Base.connection.column_exists?(:users, :name)\n  end\n\n  it \"adds and removes an index\" do\n    add_index :users, :middle_name, name: \"index_users_on_middle_name\", unique: true\n    assert ActiveRecord::Base.connection.index_exists?(:users, :middle_name, name: \"index_users_on_middle_name\")\n\n    remove_index :users, name: \"index_users_on_middle_name\"\n    refute ActiveRecord::Base.connection.index_exists?(:users, :middle_name, name: \"index_users_on_middle_name\")\n  end\n\n  it \"adds and removes timestamps\" do\n    remove_timestamps :users\n    refute ActiveRecord::Base.connection.column_exists?(:users, :created_at)\n    refute ActiveRecord::Base.connection.column_exists?(:users, :updated_at)\n\n    add_timestamps :users\n    assert ActiveRecord::Base.connection.column_exists?(:users, :created_at)\n    assert ActiveRecord::Base.connection.column_exists?(:users, :updated_at)\n  end\nend\n"
  },
  {
    "path": "test/rake_task_db_storage_full_test.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\ndescribe \"single db (db storage)\" do\n  let(:utils) { TestUtils.new }\n\n  before do\n    ActualDbSchema.config[:migrations_storage] = :db\n    utils.reset_database_yml(TestingState.db_config[\"primary\"])\n    ActiveRecord::Base.configurations = { \"test\" => TestingState.db_config[\"primary\"] }\n    ActiveRecord::Tasks::DatabaseTasks.database_configuration = { \"test\" => TestingState.db_config[\"primary\"] }\n    ActiveRecord::Base.establish_connection(**TestingState.db_config[\"primary\"])\n    utils.clear_db_storage_table\n    utils.cleanup\n  end\n\n  describe \"db:rollback_branches\" do\n    def collect_rollback_events\n      events = []\n      subscriber = ActiveSupport::Notifications.subscribe(ActualDbSchema::Instrumentation::ROLLBACK_EVENT) do |*args|\n        events << ActiveSupport::Notifications::Event.new(*args)\n      end\n\n      yield events\n    ensure\n      ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber\n    end\n\n    it \"creates the tmp/migrated folder\" do\n      refute File.exist?(utils.app_file(\"tmp/migrated\"))\n      utils.run_migrations\n      assert File.exist?(utils.app_file(\"tmp/migrated\"))\n    end\n\n    it \"migrates the migrations\" do\n      assert_empty utils.applied_migrations\n      utils.run_migrations\n      assert_equal %w[20130906111511 20130906111512], utils.applied_migrations\n    end\n\n    it \"keeps migrated migrations in tmp/migrated folder\" do\n      utils.run_migrations\n      assert_equal %w[20130906111511_first.rb 20130906111512_second.rb], utils.migrated_files\n    end\n\n    it \"rolls back the migrations in the reversed order\" do\n      utils.prepare_phantom_migrations\n      assert_empty TestingState.down\n      utils.run_migrations\n      assert_equal %i[second first], TestingState.down\n      assert_match(/\\[ActualDbSchema\\] Rolling back phantom migration/, TestingState.output)\n      assert_empty utils.migrated_files\n    end\n\n    it \"emits one instrumentation event per successful rollback\" do\n      utils.prepare_phantom_migrations\n      events = nil\n\n      collect_rollback_events do |captured_events|\n        utils.run_migrations\n        events = captured_events\n      end\n\n      assert_equal 2, events.size\n      assert_equal(%w[20130906111512 20130906111511], events.map { |event| event.payload[:version] })\n      assert_equal([false, false], events.map { |event| event.payload[:manual_mode] })\n      assert_equal([utils.primary_database, utils.primary_database], events.map { |event| event.payload[:database] })\n      assert_equal([nil, nil], events.map { |event| event.payload[:schema] })\n    end\n\n    describe \"with irreversible migration\" do\n      before do\n        utils.define_migration_file(\"20130906111513_irreversible.rb\", <<~RUBY)\n          class Irreversible < ActiveRecord::Migration[6.0]\n            def up\n              TestingState.up << :irreversible\n            end\n\n            def down\n              raise ActiveRecord::IrreversibleMigration\n            end\n          end\n        RUBY\n      end\n\n      it \"keeps track of the irreversible migrations\" do\n        utils.prepare_phantom_migrations\n        assert_equal %i[first second irreversible], TestingState.up\n        assert_empty ActualDbSchema.failed\n        utils.run_migrations\n        assert_equal(%w[20130906111513_irreversible.rb], ActualDbSchema.failed.map { |m| File.basename(m.filename) })\n        assert_match(/Error encountered during rollback:/, TestingState.output)\n        assert_match(/ActiveRecord::IrreversibleMigration/, TestingState.output)\n        assert_equal %w[20130906111513_irreversible.rb], utils.migrated_files\n      end\n\n      it \"does not emit instrumentation for failed rollbacks\" do\n        utils.prepare_phantom_migrations\n        events = nil\n\n        collect_rollback_events do |captured_events|\n          utils.run_migrations\n          events = captured_events\n        end\n\n        assert_equal(%w[20130906111512 20130906111511], events.map { |event| event.payload[:version] })\n      end\n    end\n\n    describe \"with irreversible migration is the first\" do\n      before do\n        utils.define_migration_file(\"20130906111510_irreversible.rb\", <<~RUBY)\n          class Irreversible < ActiveRecord::Migration[6.0]\n            def up\n              TestingState.up << :irreversible\n            end\n\n            def down\n              raise ActiveRecord::IrreversibleMigration\n            end\n          end\n        RUBY\n      end\n\n      it \"doesn't fail fast and has formatted output\" do\n        utils.prepare_phantom_migrations\n        assert_equal %i[irreversible first second], TestingState.up\n        assert_empty ActualDbSchema.failed\n        utils.run_migrations\n        assert_equal(%w[20130906111510_irreversible.rb], ActualDbSchema.failed.map { |m| File.basename(m.filename) })\n        assert_match(/1 phantom migration\\(s\\) could not be rolled back automatically/, TestingState.output)\n        assert_match(/Try these steps to fix and move forward:/, TestingState.output)\n        assert_match(/Below are the details of the problematic migrations:/, TestingState.output)\n        assert_match(%r{File: tmp/migrated/20130906111510_irreversible.rb}, TestingState.output)\n        assert_equal %w[20130906111510_irreversible.rb], utils.migrated_files\n      end\n    end\n\n    describe \"with acronyms defined\" do\n      before do\n        utils.define_migration_file(\"20241218064344_ts360.rb\", <<~RUBY)\n          class Ts360 < ActiveRecord::Migration[6.0]\n            def up\n              TestingState.up << :ts360\n            end\n\n            def down\n              TestingState.down << :ts360\n            end\n          end\n        RUBY\n      end\n\n      it \"rolls back the phantom migrations without failing\" do\n        utils.prepare_phantom_migrations\n        assert_equal %i[first second ts360], TestingState.up\n        assert_empty ActualDbSchema.failed\n        utils.define_acronym(\"TS360\")\n        utils.run_migrations\n        assert_equal %i[ts360 second first], TestingState.down\n        assert_empty ActualDbSchema.failed\n        assert_empty utils.migrated_files\n      end\n    end\n\n    describe \"with custom migrated folder\" do\n      before do\n        ActualDbSchema.configure { |config| config.migrated_folder = Rails.root.join(\"custom\", \"migrated\") }\n      end\n\n      after do\n        utils.remove_app_dir(\"custom/migrated\")\n        ActualDbSchema.configure { |config| config.migrated_folder = nil }\n      end\n\n      it \"creates the custom migrated folder\" do\n        refute File.exist?(utils.app_file(\"custom/migrated\"))\n        utils.run_migrations\n        assert File.exist?(utils.app_file(\"custom/migrated\"))\n      end\n\n      it \"keeps migrated migrations in the custom migrated folder\" do\n        utils.run_migrations\n        assert_equal %w[20130906111511_first.rb 20130906111512_second.rb], utils.migrated_files\n      end\n\n      it \"rolls back the migrations in the reversed order\" do\n        utils.prepare_phantom_migrations\n        assert_empty TestingState.down\n        utils.run_migrations\n        assert_equal %i[second first], TestingState.down\n        assert_match(/\\[ActualDbSchema\\] Rolling back phantom migration/, TestingState.output)\n        assert_empty utils.migrated_files\n      end\n    end\n    describe \"when app is not a git repository\" do\n      it \"doesn't show an error message\" do\n        Dir.mktmpdir do |dir|\n          Dir.chdir(dir) do\n            _out, err = capture_subprocess_io do\n              utils.prepare_phantom_migrations\n            end\n\n            refute_match(\"fatal: not a git repository\", err)\n            assert_equal \"unknown\", ActualDbSchema::Git.current_branch\n          end\n        end\n      end\n    end\n  end\n\n  after do\n    utils.clear_db_storage_table\n    ActualDbSchema.config[:migrations_storage] = :file\n  end\n\n  describe \"db:rollback_branches:manual\" do\n    it \"rolls back the migrations in the reversed order\" do\n      utils.prepare_phantom_migrations\n      assert_equal %i[first second], TestingState.up\n      assert_empty TestingState.down\n      assert_empty ActualDbSchema.failed\n      utils.simulate_input(\"y\") do\n        Rake::Task[\"db:rollback_branches:manual\"].invoke\n        Rake::Task[\"db:rollback_branches:manual\"].reenable\n      end\n      assert_equal %i[second first], TestingState.down\n      assert_empty utils.migrated_files\n    end\n\n    it \"skips migrations if the input is 'n'\" do\n      utils.prepare_phantom_migrations\n      assert_equal %i[first second], TestingState.up\n      assert_empty TestingState.down\n      assert_empty ActualDbSchema.failed\n\n      utils.simulate_input(\"n\") do\n        Rake::Task[\"db:rollback_branches:manual\"].invoke\n        Rake::Task[\"db:rollback_branches:manual\"].reenable\n      end\n      assert_empty TestingState.down\n      assert_equal %i[first second], TestingState.up\n      assert_equal %w[20130906111511_first.rb 20130906111512_second.rb], utils.migrated_files\n    end\n\n    describe \"with irreversible migration\" do\n      before do\n        utils.define_migration_file(\"20130906111513_irreversible.rb\", <<~RUBY)\n          class Irreversible < ActiveRecord::Migration[6.0]\n            def up\n              TestingState.up << :irreversible\n            end\n\n            def down\n              raise ActiveRecord::IrreversibleMigration\n            end\n          end\n        RUBY\n      end\n\n      it \"keeps track of the irreversible migrations\" do\n        utils.prepare_phantom_migrations\n        assert_equal %i[first second irreversible], TestingState.up\n        assert_empty ActualDbSchema.failed\n        utils.simulate_input(\"y\") do\n          Rake::Task[\"db:rollback_branches:manual\"].invoke\n          Rake::Task[\"db:rollback_branches:manual\"].reenable\n        end\n        assert_equal %i[second first], TestingState.down\n        assert_equal(%w[20130906111513_irreversible.rb], ActualDbSchema.failed.map { |m| File.basename(m.filename) })\n        assert_equal %w[20130906111513_irreversible.rb], utils.migrated_files\n      end\n    end\n  end\n\n  describe \"db:phantom_migrations\" do\n    it \"shows the list of phantom migrations\" do\n      ActualDbSchema::Git.stub(:current_branch, \"fix-bug\") do\n        utils.prepare_phantom_migrations\n        Rake::Task[\"db:phantom_migrations\"].invoke\n        Rake::Task[\"db:phantom_migrations\"].reenable\n        assert_match(/ Status   Migration ID    Branch   Migration File/, TestingState.output)\n        assert_match(/---------------------------------------------------/, TestingState.output)\n        assert_match(%r{   up     20130906111511  fix-bug  tmp/migrated/20130906111511_first.rb}, TestingState.output)\n        assert_match(%r{   up     20130906111512  fix-bug  tmp/migrated/20130906111512_second.rb}, TestingState.output)\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "test/rake_task_db_storage_test.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\ndescribe \"db storage\" do\n  let(:utils) { TestUtils.new }\n\n  before do\n    utils.reset_database_yml(TestingState.db_config[\"primary\"])\n    ActiveRecord::Base.configurations = { \"test\" => TestingState.db_config[\"primary\"] }\n    ActiveRecord::Tasks::DatabaseTasks.database_configuration = { \"test\" => TestingState.db_config[\"primary\"] }\n    ActiveRecord::Base.establish_connection(**TestingState.db_config[\"primary\"])\n    utils.clear_db_storage_table\n    ActualDbSchema.config[:migrations_storage] = :db\n    utils.cleanup\n  end\n\n  after do\n    utils.clear_db_storage_table\n    ActualDbSchema.config[:migrations_storage] = :file\n  end\n\n  it \"stores migrated files in the database\" do\n    utils.run_migrations\n\n    conn = ActiveRecord::Base.connection\n    assert conn.table_exists?(\"actual_db_schema_migrations\")\n\n    rows = conn.select_all(\"select version, filename from actual_db_schema_migrations\").to_a\n    versions = rows.map { |row| row[\"version\"] }.sort\n    assert_equal %w[20130906111511 20130906111512], versions\n  end\n\n  it \"rolls back phantom migrations and clears stored records\" do\n    utils.prepare_phantom_migrations\n    assert_empty TestingState.down\n\n    utils.run_migrations\n    assert_equal %i[second first], TestingState.down\n\n    rows = ActiveRecord::Base.connection.select_all(\"select version from actual_db_schema_migrations\").to_a\n    assert_empty rows\n  end\n\n  it \"materializes migration files from the database\" do\n    utils.run_migrations\n    FileUtils.rm_rf(utils.app_file(\"tmp/migrated\"))\n\n    ActualDbSchema::Store.instance.materialize_all\n    assert_equal %w[20130906111511_first.rb 20130906111512_second.rb], utils.migrated_files\n  end\nend\n"
  },
  {
    "path": "test/rake_task_delete_broken_versions_db_storage_test.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\ndescribe \"actual_db_schema:delete_broken_versions (db storage)\" do\n  let(:utils) do\n    TestUtils.new(\n      migrations_path: [\"db/migrate\", \"db/migrate_secondary\"],\n      migrated_path: [\"tmp/migrated\", \"tmp/migrated_migrate_secondary\"]\n    )\n  end\n\n  before do\n    ActualDbSchema.config[:migrations_storage] = :db\n    utils.reset_database_yml(TestingState.db_config)\n    ActiveRecord::Base.configurations = { \"test\" => TestingState.db_config }\n    ActiveRecord::Tasks::DatabaseTasks.database_configuration = { \"test\" => TestingState.db_config }\n    utils.cleanup(TestingState.db_config)\n    utils.clear_db_storage_table(TestingState.db_config)\n    utils.run_migrations\n  end\n\n  def delete_migration_files\n    remove_primary_migration_files\n    remove_secondary_migration_files\n    delete_primary_storage_entries\n    delete_secondary_storage_entries\n  end\n\n  def remove_primary_migration_files\n    utils.remove_app_dir(Rails.root.join(\"db\", \"migrate\", \"20130906111511_first_primary.rb\"))\n    utils.remove_app_dir(Rails.root.join(\"tmp\", \"migrated\", \"20130906111511_first_primary.rb\"))\n  end\n\n  def remove_secondary_migration_files\n    utils.remove_app_dir(Rails.root.join(\"db\", \"migrate_secondary\", \"20130906111514_first_secondary.rb\"))\n    utils.remove_app_dir(Rails.root.join(\"tmp\", \"migrated_migrate_secondary\", \"20130906111514_first_secondary.rb\"))\n  end\n\n  def delete_primary_storage_entries\n    ActiveRecord::Base.establish_connection(TestingState.db_config[\"primary\"])\n    ActualDbSchema::Store.instance.delete(utils.app_file(\"tmp/migrated/20130906111511_first_primary.rb\"))\n  end\n\n  def delete_secondary_storage_entries\n    ActiveRecord::Base.establish_connection(TestingState.db_config[\"secondary\"])\n    secondary_path = \"tmp/migrated_migrate_secondary/20130906111514_first_secondary.rb\"\n    ActualDbSchema::Store.instance.delete(utils.app_file(secondary_path))\n  end\n\n  describe \"when versions are provided\" do\n    before { delete_migration_files }\n\n    it \"deletes the specified broken migrations\" do\n      sql = \"SELECT COUNT(*) FROM schema_migrations\"\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"primary\"])\n      assert_equal 2, ActiveRecord::Base.connection.select_value(sql)\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"secondary\"])\n      assert_equal 2, ActiveRecord::Base.connection.select_value(sql)\n      Rake::Task[\"actual_db_schema:delete_broken_versions\"].invoke(\"20130906111511 20130906111514\")\n      Rake::Task[\"actual_db_schema:delete_broken_versions\"].reenable\n      assert_match(/\\[ActualDbSchema\\] Migration 20130906111511 was successfully deleted./, TestingState.output)\n      assert_match(/\\[ActualDbSchema\\] Migration 20130906111514 was successfully deleted./, TestingState.output)\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"primary\"])\n      assert_equal 1, ActiveRecord::Base.connection.select_value(sql)\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"secondary\"])\n      assert_equal 1, ActiveRecord::Base.connection.select_value(sql)\n    end\n\n    it \"deletes broken migrations only from the given database when specified\" do\n      sql = \"SELECT COUNT(*) FROM schema_migrations\"\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"primary\"])\n      assert_equal 2, ActiveRecord::Base.connection.select_value(sql)\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"secondary\"])\n      assert_equal 2, ActiveRecord::Base.connection.select_value(sql)\n      Rake::Task[\"actual_db_schema:delete_broken_versions\"]\n        .invoke(\"20130906111511 20130906111514\", TestingState.db_config[\"primary\"][\"database\"])\n      Rake::Task[\"actual_db_schema:delete_broken_versions\"].reenable\n      assert_match(/\\[ActualDbSchema\\] Migration 20130906111511 was successfully deleted./, TestingState.output)\n      assert_match(\n        /\\[ActualDbSchema\\] Error deleting version 20130906111514: Migration is not broken for database #{TestingState.db_config[\"primary\"][\"database\"]}./, # rubocop:disable Layout/LineLength\n        TestingState.output\n      )\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"primary\"])\n      assert_equal 1, ActiveRecord::Base.connection.select_value(sql)\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"secondary\"])\n      assert_equal 2, ActiveRecord::Base.connection.select_value(sql)\n    end\n\n    it \"prints an error message when the passed version is not broken\" do\n      Rake::Task[\"actual_db_schema:delete_broken_versions\"].invoke(\"20130906111512\")\n      Rake::Task[\"actual_db_schema:delete_broken_versions\"].reenable\n      assert_match(\n        /\\[ActualDbSchema\\] Error deleting version 20130906111512: Migration is not broken./, TestingState.output\n      )\n    end\n  end\n\n  describe \"when no versions are provided\" do\n    before { delete_migration_files }\n\n    it \"deletes all broken migrations\" do\n      delete_migration_files\n      sql = \"SELECT COUNT(*) FROM schema_migrations\"\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"primary\"])\n      assert_equal 2, ActiveRecord::Base.connection.select_value(sql)\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"secondary\"])\n      assert_equal 2, ActiveRecord::Base.connection.select_value(sql)\n      Rake::Task[\"actual_db_schema:delete_broken_versions\"].invoke\n      Rake::Task[\"actual_db_schema:delete_broken_versions\"].reenable\n      assert_match(/\\[ActualDbSchema\\] All broken versions were successfully deleted./, TestingState.output)\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"primary\"])\n      assert_equal 1, ActiveRecord::Base.connection.select_value(sql)\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"secondary\"])\n      assert_equal 1, ActiveRecord::Base.connection.select_value(sql)\n    end\n\n    it \"prints an error message if there is an error during deletion\" do\n      original_delete_all = ActualDbSchema::Migration.instance_method(:delete_all)\n      ActualDbSchema::Migration.define_method(:delete_all) do\n        raise StandardError, \"Deletion error\"\n      end\n      Rake::Task[\"actual_db_schema:delete_broken_versions\"].invoke\n      Rake::Task[\"actual_db_schema:delete_broken_versions\"].reenable\n      assert_match(/\\[ActualDbSchema\\] Error deleting all broken versions: Deletion error/, TestingState.output)\n      sql = \"SELECT COUNT(*) FROM schema_migrations\"\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"primary\"])\n      assert_equal 2, ActiveRecord::Base.connection.select_value(sql)\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"secondary\"])\n      assert_equal 2, ActiveRecord::Base.connection.select_value(sql)\n      ActualDbSchema::Migration.define_method(:delete_all, original_delete_all)\n    end\n  end\n\n  describe \"when there are no broken versions\" do\n    it \"prints a message indicating no broken versions found\" do\n      Rake::Task[\"actual_db_schema:delete_broken_versions\"].invoke\n      Rake::Task[\"actual_db_schema:delete_broken_versions\"].reenable\n      assert_match(/No broken versions found/, TestingState.output)\n    end\n  end\n\n  after do\n    utils.clear_db_storage_table(TestingState.db_config)\n    ActualDbSchema.config[:migrations_storage] = :file\n  end\nend\n"
  },
  {
    "path": "test/rake_task_delete_broken_versions_test.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\ndescribe \"actual_db_schema:delete_broken_versions\" do\n  let(:utils) do\n    TestUtils.new(\n      migrations_path: [\"db/migrate\", \"db/migrate_secondary\"],\n      migrated_path: [\"tmp/migrated\", \"tmp/migrated_migrate_secondary\"]\n    )\n  end\n\n  before do\n    utils.reset_database_yml(TestingState.db_config)\n    ActiveRecord::Base.configurations = { \"test\" => TestingState.db_config }\n    ActiveRecord::Tasks::DatabaseTasks.database_configuration = { \"test\" => TestingState.db_config }\n    utils.cleanup(TestingState.db_config)\n    utils.run_migrations\n  end\n\n  def delete_migration_files\n    utils.remove_app_dir(Rails.root.join(\"db\", \"migrate\", \"20130906111511_first_primary.rb\"))\n    utils.remove_app_dir(Rails.root.join(\"db\", \"migrate_secondary\", \"20130906111514_first_secondary.rb\"))\n    utils.remove_app_dir(Rails.root.join(\"tmp\", \"migrated\", \"20130906111511_first_primary.rb\"))\n    utils.remove_app_dir(Rails.root.join(\"tmp\", \"migrated_migrate_secondary\", \"20130906111514_first_secondary.rb\"))\n  end\n\n  describe \"when versions are provided\" do\n    before { delete_migration_files }\n\n    it \"deletes the specified broken migrations\" do\n      sql = \"SELECT COUNT(*) FROM schema_migrations\"\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"primary\"])\n      assert_equal 2, ActiveRecord::Base.connection.select_value(sql)\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"secondary\"])\n      assert_equal 2, ActiveRecord::Base.connection.select_value(sql)\n      Rake::Task[\"actual_db_schema:delete_broken_versions\"].invoke(\"20130906111511 20130906111514\")\n      Rake::Task[\"actual_db_schema:delete_broken_versions\"].reenable\n      assert_match(/\\[ActualDbSchema\\] Migration 20130906111511 was successfully deleted./, TestingState.output)\n      assert_match(/\\[ActualDbSchema\\] Migration 20130906111514 was successfully deleted./, TestingState.output)\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"primary\"])\n      assert_equal 1, ActiveRecord::Base.connection.select_value(sql)\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"secondary\"])\n      assert_equal 1, ActiveRecord::Base.connection.select_value(sql)\n    end\n\n    it \"deletes broken migrations only from the given database when specified\" do\n      sql = \"SELECT COUNT(*) FROM schema_migrations\"\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"primary\"])\n      assert_equal 2, ActiveRecord::Base.connection.select_value(sql)\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"secondary\"])\n      assert_equal 2, ActiveRecord::Base.connection.select_value(sql)\n      Rake::Task[\"actual_db_schema:delete_broken_versions\"]\n        .invoke(\"20130906111511 20130906111514\", TestingState.db_config[\"primary\"][\"database\"])\n      Rake::Task[\"actual_db_schema:delete_broken_versions\"].reenable\n      assert_match(/\\[ActualDbSchema\\] Migration 20130906111511 was successfully deleted./, TestingState.output)\n      assert_match(\n        /\\[ActualDbSchema\\] Error deleting version 20130906111514: Migration is not broken for database #{TestingState.db_config[\"primary\"][\"database\"]}./, # rubocop:disable Layout/LineLength\n        TestingState.output\n      )\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"primary\"])\n      assert_equal 1, ActiveRecord::Base.connection.select_value(sql)\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"secondary\"])\n      assert_equal 2, ActiveRecord::Base.connection.select_value(sql)\n    end\n\n    it \"prints an error message when the passed version is not broken\" do\n      Rake::Task[\"actual_db_schema:delete_broken_versions\"].invoke(\"20130906111512\")\n      Rake::Task[\"actual_db_schema:delete_broken_versions\"].reenable\n      assert_match(\n        /\\[ActualDbSchema\\] Error deleting version 20130906111512: Migration is not broken./, TestingState.output\n      )\n    end\n  end\n\n  describe \"when no versions are provided\" do\n    before { delete_migration_files }\n\n    it \"deletes all broken migrations\" do\n      delete_migration_files\n      sql = \"SELECT COUNT(*) FROM schema_migrations\"\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"primary\"])\n      assert_equal 2, ActiveRecord::Base.connection.select_value(sql)\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"secondary\"])\n      assert_equal 2, ActiveRecord::Base.connection.select_value(sql)\n      Rake::Task[\"actual_db_schema:delete_broken_versions\"].invoke\n      Rake::Task[\"actual_db_schema:delete_broken_versions\"].reenable\n      assert_match(/\\[ActualDbSchema\\] All broken versions were successfully deleted./, TestingState.output)\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"primary\"])\n      assert_equal 1, ActiveRecord::Base.connection.select_value(sql)\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"secondary\"])\n      assert_equal 1, ActiveRecord::Base.connection.select_value(sql)\n    end\n\n    it \"prints an error message if there is an error during deletion\" do\n      original_delete_all = ActualDbSchema::Migration.instance_method(:delete_all)\n      ActualDbSchema::Migration.define_method(:delete_all) do\n        raise StandardError, \"Deletion error\"\n      end\n      Rake::Task[\"actual_db_schema:delete_broken_versions\"].invoke\n      Rake::Task[\"actual_db_schema:delete_broken_versions\"].reenable\n      assert_match(/\\[ActualDbSchema\\] Error deleting all broken versions: Deletion error/, TestingState.output)\n      sql = \"SELECT COUNT(*) FROM schema_migrations\"\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"primary\"])\n      assert_equal 2, ActiveRecord::Base.connection.select_value(sql)\n      ActiveRecord::Base.establish_connection(TestingState.db_config[\"secondary\"])\n      assert_equal 2, ActiveRecord::Base.connection.select_value(sql)\n      ActualDbSchema::Migration.define_method(:delete_all, original_delete_all)\n    end\n  end\n\n  describe \"when there are no broken versions\" do\n    it \"prints a message indicating no broken versions found\" do\n      Rake::Task[\"actual_db_schema:delete_broken_versions\"].invoke\n      Rake::Task[\"actual_db_schema:delete_broken_versions\"].reenable\n      assert_match(/No broken versions found/, TestingState.output)\n    end\n  end\nend\n"
  },
  {
    "path": "test/rake_task_git_hooks_install_db_storage_test.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\ndescribe \"actual_db_schema:install_git_hooks (db storage)\" do\n  let(:utils) { TestUtils.new }\n  let(:hook_path) { utils.app_file(\".git/hooks/post-checkout\") }\n\n  before do\n    ActualDbSchema.config[:migrations_storage] = :db\n    utils.clear_db_storage_table\n    FileUtils.mkdir_p(utils.app_file(\".git/hooks\"))\n    Rails.application.load_tasks\n    ActualDbSchema.config[:git_hooks_enabled] = true\n  end\n\n  after do\n    FileUtils.rm_rf(utils.app_file(\".git/hooks\"))\n    Rake::Task[\"actual_db_schema:install_git_hooks\"].reenable\n    utils.clear_db_storage_table\n    ActualDbSchema.config[:migrations_storage] = :file\n  end\n\n  describe \"when .git/hooks directory is missing\" do\n    before do\n      FileUtils.rm_rf(utils.app_file(\".git/hooks\"))\n    end\n\n    it \"does not attempt installation and shows an error message\" do\n      utils.simulate_input(\"1\") do\n        Rake::Task[\"actual_db_schema:install_git_hooks\"].invoke\n      end\n      refute File.exist?(hook_path)\n      assert_match(\n        %r{\\[ActualDbSchema\\] .git/hooks directory not found. Please ensure this is a Git repository.},\n        TestingState.output\n      )\n    end\n  end\n\n  describe \"when user chooses rollback\" do\n    it \"installs the rollback snippet in post-checkout\" do\n      refute File.exist?(hook_path)\n      utils.simulate_input(\"1\") do\n        Rake::Task[\"actual_db_schema:install_git_hooks\"].invoke\n      end\n      assert File.exist?(hook_path)\n      contents = File.read(hook_path)\n      assert_includes(contents, \"db:rollback_branches\")\n      refute_includes(contents, \"db:migrate\")\n    end\n  end\n\n  describe \"when user chooses migrate\" do\n    it \"installs the migrate snippet in post-checkout\" do\n      refute File.exist?(hook_path)\n      utils.simulate_input(\"2\") do\n        Rake::Task[\"actual_db_schema:install_git_hooks\"].invoke\n      end\n      assert File.exist?(hook_path)\n      contents = File.read(hook_path)\n      assert_includes(contents, \"db:migrate\")\n      refute_includes(contents, \"db:rollback_branches\")\n    end\n  end\n\n  describe \"when user chooses none\" do\n    it \"skips installing the post-checkout hook\" do\n      refute File.exist?(hook_path)\n      utils.simulate_input(\"3\") do\n        Rake::Task[\"actual_db_schema:install_git_hooks\"].invoke\n      end\n      refute File.exist?(hook_path)\n      assert_match(/\\[ActualDbSchema\\] Skipping git hook installation\\./, TestingState.output)\n    end\n  end\n\n  describe \"when post-checkout hook already exists\" do\n    before do\n      File.write(hook_path, \"#!/usr/bin/env bash\\n# Existing content\\n\")\n    end\n\n    it \"appends content if user decides to overwrite\" do\n      utils.simulate_input(\"1\\ny\") do\n        Rake::Task[\"actual_db_schema:install_git_hooks\"].invoke\n      end\n      contents = File.read(hook_path)\n      assert_includes(contents, \"db:rollback_branches\")\n      assert_includes(contents, \"# Existing content\")\n    end\n\n    it \"does not change file and shows manual instructions if user declines overwrite\" do\n      utils.simulate_input(\"2\\nn\") do\n        Rake::Task[\"actual_db_schema:install_git_hooks\"].invoke\n      end\n      contents = File.read(hook_path)\n      refute_includes(contents, \"db:migrate\")\n      assert_includes(contents, \"# Existing content\")\n      assert_match(/\\[ActualDbSchema\\] You can follow these steps to manually install the hook/, TestingState.output)\n    end\n  end\n\n  describe \"existing post-checkout hook with markers\" do\n    before do\n      File.write(\n        hook_path,\n        <<~BASH\n          #!/usr/bin/env bash\n          echo \"some existing code\"\n\n          # >>> BEGIN ACTUAL_DB_SCHEMA\n          echo \"old snippet\"\n          # <<< END ACTUAL_DB_SCHEMA\n        BASH\n      )\n    end\n\n    it \"updates the snippet if markers exist\" do\n      utils.simulate_input(\"2\") do\n        Rake::Task[\"actual_db_schema:install_git_hooks\"].invoke\n      end\n      contents = File.read(hook_path)\n      refute_includes(contents, \"old snippet\")\n      assert_includes(contents, \"db:migrate\")\n      assert_includes(contents, \"some existing code\")\n    end\n  end\nend\n"
  },
  {
    "path": "test/rake_task_git_hooks_install_test.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\ndescribe \"actual_db_schema:install_git_hooks\" do\n  let(:utils) { TestUtils.new }\n  let(:hook_path) { utils.app_file(\".git/hooks/post-checkout\") }\n\n  before do\n    FileUtils.mkdir_p(utils.app_file(\".git/hooks\"))\n    Rails.application.load_tasks\n    ActualDbSchema.config[:git_hooks_enabled] = true\n  end\n\n  after do\n    FileUtils.rm_rf(utils.app_file(\".git/hooks\"))\n    Rake::Task[\"actual_db_schema:install_git_hooks\"].reenable\n  end\n\n  describe \"when .git/hooks directory is missing\" do\n    before do\n      FileUtils.rm_rf(utils.app_file(\".git/hooks\"))\n    end\n\n    it \"does not attempt installation and shows an error message\" do\n      utils.simulate_input(\"1\") do\n        Rake::Task[\"actual_db_schema:install_git_hooks\"].invoke\n      end\n      refute File.exist?(hook_path)\n      assert_match(\n        %r{\\[ActualDbSchema\\] .git/hooks directory not found. Please ensure this is a Git repository.},\n        TestingState.output\n      )\n    end\n  end\n\n  describe \"when user chooses rollback\" do\n    it \"installs the rollback snippet in post-checkout\" do\n      refute File.exist?(hook_path)\n      utils.simulate_input(\"1\") do\n        Rake::Task[\"actual_db_schema:install_git_hooks\"].invoke\n      end\n      assert File.exist?(hook_path)\n      contents = File.read(hook_path)\n      assert_includes(contents, \"db:rollback_branches\")\n      refute_includes(contents, \"db:migrate\")\n    end\n  end\n\n  describe \"when user chooses migrate\" do\n    it \"installs the migrate snippet in post-checkout\" do\n      refute File.exist?(hook_path)\n      utils.simulate_input(\"2\") do\n        Rake::Task[\"actual_db_schema:install_git_hooks\"].invoke\n      end\n      assert File.exist?(hook_path)\n      contents = File.read(hook_path)\n      assert_includes(contents, \"db:migrate\")\n      refute_includes(contents, \"db:rollback_branches\")\n    end\n  end\n\n  describe \"when user chooses none\" do\n    it \"skips installing the post-checkout hook\" do\n      refute File.exist?(hook_path)\n      utils.simulate_input(\"3\") do\n        Rake::Task[\"actual_db_schema:install_git_hooks\"].invoke\n      end\n      refute File.exist?(hook_path)\n      assert_match(/\\[ActualDbSchema\\] Skipping git hook installation\\./, TestingState.output)\n    end\n  end\n\n  describe \"when post-checkout hook already exists\" do\n    before do\n      File.write(hook_path, \"#!/usr/bin/env bash\\n# Existing content\\n\")\n    end\n\n    it \"appends content if user decides to overwrite\" do\n      utils.simulate_input(\"1\\ny\") do\n        Rake::Task[\"actual_db_schema:install_git_hooks\"].invoke\n      end\n      contents = File.read(hook_path)\n      assert_includes(contents, \"db:rollback_branches\")\n      assert_includes(contents, \"# Existing content\")\n    end\n\n    it \"does not change file and shows manual instructions if user declines overwrite\" do\n      utils.simulate_input(\"2\\nn\") do\n        Rake::Task[\"actual_db_schema:install_git_hooks\"].invoke\n      end\n      contents = File.read(hook_path)\n      refute_includes(contents, \"db:migrate\")\n      assert_includes(contents, \"# Existing content\")\n      assert_match(/\\[ActualDbSchema\\] You can follow these steps to manually install the hook/, TestingState.output)\n    end\n  end\n\n  describe \"existing post-checkout hook with markers\" do\n    before do\n      File.write(\n        hook_path,\n        <<~BASH\n          #!/usr/bin/env bash\n          echo \"some existing code\"\n\n          # >>> BEGIN ACTUAL_DB_SCHEMA\n          echo \"old snippet\"\n          # <<< END ACTUAL_DB_SCHEMA\n        BASH\n      )\n    end\n\n    it \"updates the snippet if markers exist\" do\n      utils.simulate_input(\"2\") do\n        Rake::Task[\"actual_db_schema:install_git_hooks\"].invoke\n      end\n      contents = File.read(hook_path)\n      refute_includes(contents, \"old snippet\")\n      assert_includes(contents, \"db:migrate\")\n      assert_includes(contents, \"some existing code\")\n    end\n  end\nend\n"
  },
  {
    "path": "test/rake_task_multi_tenant_db_storage_test.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\ndescribe \"multi-tenant db support (db storage)\" do\n  let(:utils) { TestUtils.new }\n\n  before do\n    skip \"Skipping multi-tenant tests for sqlite3\" if TestingState.db_config[\"primary\"][\"adapter\"] == \"sqlite3\"\n\n    ActualDbSchema.config[:migrations_storage] = :db\n    utils.reset_database_yml(TestingState.db_config[\"primary\"])\n    ActiveRecord::Base.configurations = { \"test\" => TestingState.db_config[\"primary\"] }\n    ActiveRecord::Tasks::DatabaseTasks.database_configuration = { \"test\" => TestingState.db_config[\"primary\"] }\n    ActiveRecord::Base.establish_connection(**TestingState.db_config[\"primary\"])\n    utils.clear_db_storage_table\n\n    if ActiveRecord::Base.connection.adapter_name =~ /postgresql/i\n      ActiveRecord::Base.connection.execute(\"CREATE SCHEMA IF NOT EXISTS tenant1\")\n      ActualDbSchema.config[:multi_tenant_schemas] = -> { %w[public tenant1] }\n    elsif ActiveRecord::Base.connection.adapter_name =~ /mysql/i\n      ActiveRecord::Base.connection.execute(\"CREATE DATABASE IF NOT EXISTS tenant1\")\n      ActualDbSchema.config[:multi_tenant_schemas] = -> { [TestingState.db_config[\"primary\"][\"database\"], \"tenant1\"] }\n    end\n\n    utils.cleanup\n  end\n\n  after do\n    if ActiveRecord::Base.connection.adapter_name =~ /postgresql/i\n      ActiveRecord::Base.connection.execute(\"DROP SCHEMA IF EXISTS tenant1 CASCADE\")\n    elsif ActiveRecord::Base.connection.adapter_name =~ /mysql/i\n      ActiveRecord::Base.connection.execute(\"DROP DATABASE IF EXISTS tenant1\")\n    end\n\n    ActualDbSchema.config[:multi_tenant_schemas] = nil\n    utils.clear_db_storage_table\n    ActualDbSchema.config[:migrations_storage] = :file\n  end\n\n  describe \"db:rollback_branches\" do\n    it \"creates the tmp/migrated folder\" do\n      refute File.exist?(utils.app_file(\"tmp/migrated\"))\n      utils.run_migrations\n      assert File.exist?(utils.app_file(\"tmp/migrated\"))\n    end\n\n    it \"migrates the migrations\" do\n      assert_empty utils.applied_migrations\n      utils.run_migrations\n      assert_equal %w[20130906111511 20130906111512], utils.applied_migrations\n    end\n\n    it \"keeps migrated migrations in tmp/migrated folder\" do\n      utils.run_migrations\n      assert_equal %w[20130906111511_first.rb 20130906111512_second.rb], utils.migrated_files\n    end\n\n    it \"rolls back phantom migrations both in public (or primary) schema and tenant1\" do\n      utils.prepare_phantom_migrations\n      assert_empty TestingState.down\n      utils.run_migrations\n      assert_equal %i[second first second first], TestingState.down\n      primary_schema = {\n        \"postgresql\" => \"public\",\n        \"mysql2\" => TestingState.db_config[\"primary\"][\"database\"]\n      }.fetch(TestingState.db_config[\"primary\"][\"adapter\"])\n      assert_match(/\\[ActualDbSchema\\] #{primary_schema}: Rolling back phantom migration/, TestingState.output)\n      assert_match(/\\[ActualDbSchema\\] tenant1: Rolling back phantom migration/, TestingState.output)\n    end\n  end\n\n  describe \"with irreversible migration\" do\n    before do\n      utils.define_migration_file(\"20130906111513_irreversible.rb\", <<~RUBY)\n        class Irreversible < ActiveRecord::Migration[6.0]\n          def up\n            TestingState.up << :irreversible\n          end\n\n          def down\n            raise ActiveRecord::IrreversibleMigration\n          end\n        end\n      RUBY\n    end\n\n    it \"keeps track of the irreversible migrations\" do\n      utils.prepare_phantom_migrations\n      assert_equal %i[first second irreversible first second irreversible], TestingState.up\n      assert_empty ActualDbSchema.failed\n      utils.run_migrations\n      failed = ActualDbSchema.failed.map { |m| File.basename(m.filename) }\n      assert_equal(%w[20130906111513_irreversible.rb 20130906111513_irreversible.rb], failed)\n      assert_includes utils.migrated_files, \"20130906111513_irreversible.rb\"\n    end\n  end\n\n  describe \"db:rollback_branches:manual\" do\n    it \"rolls back phantom migrations both in public (or primary) schema and tenant1\" do\n      utils.prepare_phantom_migrations\n      assert_equal %i[first second first second], TestingState.up\n      assert_empty TestingState.down\n      assert_empty ActualDbSchema.failed\n      utils.simulate_input(\"y\") do\n        Rake::Task[\"db:rollback_branches:manual\"].invoke\n        Rake::Task[\"db:rollback_branches:manual\"].reenable\n      end\n      assert_equal %i[second first second first], TestingState.down\n      assert_empty utils.migrated_files\n    end\n\n    it \"skips migrations if the input is 'n'\" do\n      utils.prepare_phantom_migrations\n      assert_equal %i[first second first second], TestingState.up\n      assert_empty TestingState.down\n      assert_empty ActualDbSchema.failed\n\n      utils.simulate_input(\"n\") do\n        Rake::Task[\"db:rollback_branches:manual\"].invoke\n        Rake::Task[\"db:rollback_branches:manual\"].reenable\n      end\n      assert_empty TestingState.down\n      assert_equal %i[first second first second], TestingState.up\n      assert_equal %w[20130906111511_first.rb 20130906111512_second.rb], utils.migrated_files\n    end\n\n    describe \"with irreversible migration\" do\n      before do\n        utils.define_migration_file(\"20130906111513_irreversible.rb\", <<~RUBY)\n          class Irreversible < ActiveRecord::Migration[6.0]\n            def up\n              TestingState.up << :irreversible\n            end\n\n            def down\n              raise ActiveRecord::IrreversibleMigration\n            end\n          end\n        RUBY\n      end\n\n      it \"keeps track of the irreversible migrations\" do\n        utils.prepare_phantom_migrations\n        assert_equal %i[first second irreversible first second irreversible], TestingState.up\n        assert_empty ActualDbSchema.failed\n        utils.simulate_input(\"y\") do\n          Rake::Task[\"db:rollback_branches:manual\"].invoke\n          Rake::Task[\"db:rollback_branches:manual\"].reenable\n        end\n        assert_equal %i[second first second first], TestingState.down\n        failed = ActualDbSchema.failed.map { |m| File.basename(m.filename) }\n        assert_equal(%w[20130906111513_irreversible.rb 20130906111513_irreversible.rb], failed)\n        assert_equal %w[20130906111513_irreversible.rb], utils.migrated_files\n      end\n    end\n  end\n\n  describe \"db:phantom_migrations\" do\n    it \"shows the list of phantom migrations\" do\n      ActualDbSchema::Git.stub(:current_branch, \"fix-bug\") do\n        utils.prepare_phantom_migrations\n        Rake::Task[\"db:phantom_migrations\"].invoke\n        Rake::Task[\"db:phantom_migrations\"].reenable\n        assert_match(/ Status   Migration ID    Branch   Migration File/, TestingState.output)\n        assert_match(/---------------------------------------------------/, TestingState.output)\n        assert_match(%r{   up     20130906111511  fix-bug  tmp/migrated/20130906111511_first.rb}, TestingState.output)\n        assert_match(%r{   up     20130906111512  fix-bug  tmp/migrated/20130906111512_second.rb}, TestingState.output)\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "test/rake_task_multi_tenant_test.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\ndescribe \"multi-tenant db support\" do\n  let(:utils) { TestUtils.new }\n\n  before do\n    skip \"Skipping multi-tenant tests for sqlite3\" if TestingState.db_config[\"primary\"][\"adapter\"] == \"sqlite3\"\n\n    utils.reset_database_yml(TestingState.db_config[\"primary\"])\n    ActiveRecord::Base.configurations = { \"test\" => TestingState.db_config[\"primary\"] }\n    ActiveRecord::Tasks::DatabaseTasks.database_configuration = { \"test\" => TestingState.db_config[\"primary\"] }\n    ActiveRecord::Base.establish_connection(**TestingState.db_config[\"primary\"])\n\n    if ActiveRecord::Base.connection.adapter_name =~ /postgresql/i\n      ActiveRecord::Base.connection.execute(\"CREATE SCHEMA IF NOT EXISTS tenant1\")\n      ActualDbSchema.config[:multi_tenant_schemas] = -> { %w[public tenant1] }\n    elsif ActiveRecord::Base.connection.adapter_name =~ /mysql/i\n      ActiveRecord::Base.connection.execute(\"CREATE DATABASE IF NOT EXISTS tenant1\")\n      ActualDbSchema.config[:multi_tenant_schemas] = -> { [TestingState.db_config[\"primary\"][\"database\"], \"tenant1\"] }\n    end\n\n    utils.cleanup\n  end\n\n  after do\n    if ActiveRecord::Base.connection.adapter_name =~ /postgresql/i\n      ActiveRecord::Base.connection.execute(\"DROP SCHEMA IF EXISTS tenant1 CASCADE\")\n    elsif ActiveRecord::Base.connection.adapter_name =~ /mysql/i\n      ActiveRecord::Base.connection.execute(\"DROP DATABASE IF EXISTS tenant1\")\n    end\n\n    ActualDbSchema.config[:multi_tenant_schemas] = nil\n  end\n\n  describe \"db:rollback_branches\" do\n    it \"creates the tmp/migrated folder\" do\n      refute File.exist?(utils.app_file(\"tmp/migrated\"))\n      utils.run_migrations\n      assert File.exist?(utils.app_file(\"tmp/migrated\"))\n    end\n\n    it \"migrates the migrations\" do\n      assert_empty utils.applied_migrations\n      utils.run_migrations\n      assert_equal %w[20130906111511 20130906111512], utils.applied_migrations\n    end\n\n    it \"keeps migrated migrations in tmp/migrated folder\" do\n      utils.run_migrations\n      assert_equal %w[20130906111511_first.rb 20130906111512_second.rb], utils.migrated_files\n    end\n\n    it \"rolls back phantom migrations both in public (or primary) schema and tenant1\" do\n      utils.prepare_phantom_migrations\n      assert_empty TestingState.down\n      utils.run_migrations\n      assert_equal %i[second first second first], TestingState.down\n      primary_schema = {\n        \"postgresql\" => \"public\",\n        \"mysql2\" => TestingState.db_config[\"primary\"][\"database\"]\n      }.fetch(TestingState.db_config[\"primary\"][\"adapter\"])\n      assert_match(/\\[ActualDbSchema\\] #{primary_schema}: Rolling back phantom migration/, TestingState.output)\n      assert_match(/\\[ActualDbSchema\\] tenant1: Rolling back phantom migration/, TestingState.output)\n      assert_empty utils.migrated_files\n    end\n  end\n\n  describe \"with irreversible migration\" do\n    before do\n      utils.define_migration_file(\"20130906111513_irreversible.rb\", <<~RUBY)\n        class Irreversible < ActiveRecord::Migration[6.0]\n          def up\n            TestingState.up << :irreversible\n          end\n\n          def down\n            raise ActiveRecord::IrreversibleMigration\n          end\n        end\n      RUBY\n    end\n\n    it \"keeps track of the irreversible migrations\" do\n      utils.prepare_phantom_migrations\n      assert_equal %i[first second irreversible first second irreversible], TestingState.up\n      assert_empty ActualDbSchema.failed\n      utils.run_migrations\n      failed = ActualDbSchema.failed.map { |m| File.basename(m.filename) }\n      assert_equal(%w[20130906111513_irreversible.rb 20130906111513_irreversible.rb], failed)\n      assert_equal %w[20130906111513_irreversible.rb], utils.migrated_files\n    end\n  end\n\n  describe \"db:rollback_branches:manual\" do\n    it \"rolls back phantom migrations both in public (or primary) schema and tenant1\" do\n      utils.prepare_phantom_migrations\n      assert_equal %i[first second first second], TestingState.up\n      assert_empty TestingState.down\n      assert_empty ActualDbSchema.failed\n      utils.simulate_input(\"y\") do\n        Rake::Task[\"db:rollback_branches:manual\"].invoke\n        Rake::Task[\"db:rollback_branches:manual\"].reenable\n      end\n      assert_equal %i[second first second first], TestingState.down\n      assert_empty utils.migrated_files\n    end\n\n    it \"skips migrations if the input is 'n'\" do\n      utils.prepare_phantom_migrations\n      assert_equal %i[first second first second], TestingState.up\n      assert_empty TestingState.down\n      assert_empty ActualDbSchema.failed\n\n      utils.simulate_input(\"n\") do\n        Rake::Task[\"db:rollback_branches:manual\"].invoke\n        Rake::Task[\"db:rollback_branches:manual\"].reenable\n      end\n      assert_empty TestingState.down\n      assert_equal %i[first second first second], TestingState.up\n      assert_equal %w[20130906111511_first.rb 20130906111512_second.rb], utils.migrated_files\n    end\n\n    describe \"with irreversible migration\" do\n      before do\n        utils.define_migration_file(\"20130906111513_irreversible.rb\", <<~RUBY)\n          class Irreversible < ActiveRecord::Migration[6.0]\n            def up\n              TestingState.up << :irreversible\n            end\n\n            def down\n              raise ActiveRecord::IrreversibleMigration\n            end\n          end\n        RUBY\n      end\n\n      it \"keeps track of the irreversible migrations\" do\n        utils.prepare_phantom_migrations\n        assert_equal %i[first second irreversible first second irreversible], TestingState.up\n        assert_empty ActualDbSchema.failed\n        utils.simulate_input(\"y\") do\n          Rake::Task[\"db:rollback_branches:manual\"].invoke\n          Rake::Task[\"db:rollback_branches:manual\"].reenable\n        end\n        assert_equal %i[second first second first], TestingState.down\n        failed = ActualDbSchema.failed.map { |m| File.basename(m.filename) }\n        assert_equal(%w[20130906111513_irreversible.rb 20130906111513_irreversible.rb], failed)\n        assert_equal %w[20130906111513_irreversible.rb], utils.migrated_files\n      end\n    end\n  end\n\n  describe \"db:phantom_migrations\" do\n    it \"shows the list of phantom migrations\" do\n      ActualDbSchema::Git.stub(:current_branch, \"fix-bug\") do\n        utils.prepare_phantom_migrations\n        Rake::Task[\"db:phantom_migrations\"].invoke\n        Rake::Task[\"db:phantom_migrations\"].reenable\n        assert_match(/ Status   Migration ID    Branch   Migration File/, TestingState.output)\n        assert_match(/---------------------------------------------------/, TestingState.output)\n        assert_match(%r{   up     20130906111511  fix-bug  tmp/migrated/20130906111511_first.rb}, TestingState.output)\n        assert_match(%r{   up     20130906111512  fix-bug  tmp/migrated/20130906111512_second.rb}, TestingState.output)\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "test/rake_task_schema_diff_db_storage_test.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\ndescribe \"actual_db_schema:diff_schema_with_migrations (db storage)\" do\n  let(:utils) { TestUtils.new }\n\n  before do\n    ActualDbSchema.config[:migrations_storage] = :db\n    utils.reset_database_yml(TestingState.db_config[\"primary\"])\n    ActiveRecord::Base.configurations = { \"test\" => TestingState.db_config[\"primary\"] }\n    ActiveRecord::Tasks::DatabaseTasks.database_configuration = { \"test\" => TestingState.db_config[\"primary\"] }\n    ActiveRecord::Base.establish_connection(**TestingState.db_config[\"primary\"])\n    utils.clear_db_storage_table\n    utils.cleanup\n\n    utils.define_migration_file(\"20250124084321_create_users.rb\", <<~RUBY)\n      class CreateUsers < ActiveRecord::Migration[6.0]\n        def change\n          create_table :users do |t|\n            t.string :name\n            t.string :middle_name\n            t.timestamps\n          end\n\n          add_index :users, :name, name: \"index_users_on_name\", unique: true\n        end\n      end\n    RUBY\n    utils.define_migration_file(\"20250124084322_create_products.rb\", <<~RUBY)\n      class CreateProducts < ActiveRecord::Migration[6.0]\n        def change\n          create_table :products do |t|\n            t.string :name\n            t.decimal :price, precision: 10, scale: 2\n            t.timestamps\n          end\n        end\n      end\n    RUBY\n    utils.run_migrations\n\n    ActualDbSchema::SchemaDiff.define_method(:old_schema_content) do\n      <<~RUBY\n        ActiveRecord::Schema[6.0].define(version: 20250124084322) do\n          create_table \"products\", force: :cascade do |t|\n            t.string \"name\"\n            t.decimal \"price\", precision: 10, scale: 2\n            t.datetime \"created_at\", null: false\n            t.datetime \"updated_at\", null: false\n          end\n\n          create_table \"users\", force: :cascade do |t|\n            t.string \"name\"\n            t.string \"middle_name\"\n            t.datetime \"created_at\", null: false\n            t.datetime \"updated_at\", null: false\n            t.index [\"name\"], name: \"index_users_on_name\", unique: true\n          end\n        end\n      RUBY\n    end\n  end\n\n  after do\n    utils.define_migration_file(\"20250124084323_drop_users.rb\", <<~RUBY)\n      class DropUsers < ActiveRecord::Migration[6.0]\n        def change\n          drop_table :users\n        end\n      end\n    RUBY\n    utils.define_migration_file(\"20250124084324_drop_products.rb\", <<~RUBY)\n      class DropProducts < ActiveRecord::Migration[6.0]\n        def change\n          drop_table :products, if_exists: true\n        end\n      end\n    RUBY\n    utils.run_migrations\n    utils.clear_db_storage_table\n    ActualDbSchema.config[:migrations_storage] = :file\n  end\n\n  def invoke_rake_task\n    Rake::Task[\"actual_db_schema:diff_schema_with_migrations\"].invoke(\n      \"test/dummy_app/db/schema.rb\", \"test/dummy_app/db/migrate\"\n    )\n    Rake::Task[\"actual_db_schema:diff_schema_with_migrations\"].reenable\n  end\n\n  def migration_path(file_name)\n    File.join(\"test/dummy_app/db/migrate\", file_name)\n  end\n\n  it \"annotates adding a column\" do\n    file_name = \"20250124084325_add_surname_to_users.rb\"\n    utils.define_migration_file(file_name, <<~RUBY)\n      class AddSurnameToUsers < ActiveRecord::Migration[6.0]\n        def change\n          add_column :users, :surname, :string\n        end\n      end\n    RUBY\n\n    utils.run_migrations\n    invoke_rake_task\n    assert_match(\n      %r{\\+    t\\.string \"surname\" // #{migration_path(file_name)} //},\n      TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n    )\n  end\n\n  it \"annotates removing a column\" do\n    file_name = \"20250124084326_remove_middle_name_from_users.rb\"\n    utils.define_migration_file(file_name, <<~RUBY)\n      class RemoveMiddleNameFromUsers < ActiveRecord::Migration[6.0]\n        def change\n          remove_column :users, :middle_name\n        end\n      end\n    RUBY\n\n    utils.run_migrations\n    invoke_rake_task\n    assert_match(\n      %r{-    t\\.string \"middle_name\" // #{migration_path(file_name)} //},\n      TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n    )\n  end\n\n  it \"annotates changing a column\" do\n    file_name = \"20250124084327_change_price_precision_in_products.rb\"\n    utils.define_migration_file(file_name, <<~RUBY)\n      class ChangePricePrecisionInProducts < ActiveRecord::Migration[6.0]\n        def change\n          change_column :products, :price, :decimal, precision: 15, scale: 2\n        end\n      end\n    RUBY\n\n    utils.run_migrations\n    invoke_rake_task\n    assert_match(\n      %r{-    t\\.decimal \"price\", precision: 10, scale: 2 // #{migration_path(file_name)} //},\n      TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n    )\n    assert_match(\n      %r{\\+    t\\.decimal \"price\", precision: 15, scale: 2 // #{migration_path(file_name)} //},\n      TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n    )\n  end\n\n  it \"annotates renaming a column\" do\n    file_name = \"20250124084328_rename_name_to_full_name_in_users.rb\"\n    utils.define_migration_file(file_name, <<~RUBY)\n      class RenameNameToFullNameInUsers < ActiveRecord::Migration[6.0]\n        def change\n          rename_column :users, :name, :full_name\n        end\n      end\n    RUBY\n\n    utils.run_migrations\n    invoke_rake_task\n    assert_match(\n      %r{-    t\\.string \"name\" // #{migration_path(file_name)} //},\n      TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n    )\n    assert_match(\n      %r{\\+    t\\.string \"full_name\" // #{migration_path(file_name)} //},\n      TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n    )\n  end\n\n  it \"annotates adding an index\" do\n    file_name = \"20250124084329_add_index_on_users_middle_name.rb\"\n    utils.define_migration_file(file_name, <<~RUBY)\n      class AddIndexOnUsersMiddleName < ActiveRecord::Migration[6.0]\n        def change\n          add_index :users, :middle_name, name: \"index_users_on_middle_name\", unique: true\n        end\n      end\n    RUBY\n\n    utils.run_migrations\n    invoke_rake_task\n    assert_match(\n      %r{\\+    t\\.index \\[\"middle_name\"\\], name: \"index_users_on_middle_name\", unique: true // #{migration_path(file_name)} //}, # rubocop:disable Layout/LineLength\n      TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n    )\n  end\n\n  it \"annotates removing an index\" do\n    file_name = \"20250124084330_remove_index_on_users_name.rb\"\n    utils.define_migration_file(file_name, <<~RUBY)\n      class RemoveIndexOnUsersName < ActiveRecord::Migration[6.0]\n        def change\n          remove_index :users, name: \"index_users_on_name\"\n        end\n      end\n    RUBY\n    utils.run_migrations\n    invoke_rake_task\n    assert_match(\n      %r{-    t\\.index \\[\"name\"\\], name: \"index_users_on_name\", unique: true // #{migration_path(file_name)} //},\n      TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n    )\n  end\n\n  it \"annotates renaming an index\" do\n    file_name = \"20250124084331_rename_index_on_users_name.rb\"\n    utils.define_migration_file(file_name, <<~RUBY)\n      class RenameIndexOnUsersName < ActiveRecord::Migration[6.0]\n        def change\n          rename_index :users, \"index_users_on_name\", \"index_users_on_user_name\"\n        end\n      end\n    RUBY\n    utils.run_migrations\n    invoke_rake_task\n    assert_match(\n      %r{-    t\\.index \\[\"name\"\\], name: \"index_users_on_name\", unique: true // #{migration_path(file_name)} //},\n      TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n    )\n    assert_match(\n      %r{\\+    t\\.index \\[\"name\"\\], name: \"index_users_on_user_name\", unique: true // #{migration_path(file_name)} //},\n      TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n    )\n  end\n\n  it \"annotates creating a new table\" do\n    file_name = \"20250124084332_create_categories.rb\"\n    utils.define_migration_file(file_name, <<~RUBY)\n      class CreateCategories < ActiveRecord::Migration[6.0]\n        def change\n          create_table :categories do |t|\n            t.string :title\n            t.timestamps\n          end\n        end\n      end\n    RUBY\n\n    utils.run_migrations\n    invoke_rake_task\n    assert_match(\n      %r{\\+    create_table \"categories\", force: :cascade do |t| // #{migration_path(file_name)} //},\n      TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n    )\n\n    utils.define_migration_file(\"20250124084333_drop_categories.rb\", <<~RUBY)\n      class DropCategories < ActiveRecord::Migration[6.0]\n        def change\n          drop_table :categories\n        end\n      end\n    RUBY\n    utils.run_migrations\n  end\n\n  it \"annotates dropping a table\" do\n    file_name = \"20250124084334_drop_products_table.rb\"\n    utils.define_migration_file(file_name, <<~RUBY)\n      class DropProductsTable < ActiveRecord::Migration[6.0]\n        def change\n          drop_table :products\n        end\n      end\n    RUBY\n\n    utils.run_migrations\n    invoke_rake_task\n    assert_match(\n      %r{-    create_table \"products\", force: :cascade do |t| // #{migration_path(file_name)} //},\n      TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n    )\n  end\n\n  it \"processes phantom migrations from tmp/migrated folders\" do\n    file_name = \"20250124084335_phantom.rb\"\n    utils.define_migration_file(file_name, <<~RUBY)\n      class Phantom < ActiveRecord::Migration[6.0]\n        disable_ddl_transaction!\n\n        def up\n          TestingState.up << :phantom\n        end\n\n        def down\n          add_column :users, :email, :string\n          raise ActiveRecord::IrreversibleMigration\n        end\n      end\n    RUBY\n\n    utils.run_migrations\n    utils.remove_app_dir(Rails.root.join(\"db\", \"migrate\", file_name))\n    utils.run_migrations\n    invoke_rake_task\n    assert_match(\n      %r{\\+    t\\.string \"email\" // #{File.join(\"test/dummy_app/tmp/migrated\", file_name)} //},\n      TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n    )\n  end\nend\n"
  },
  {
    "path": "test/rake_task_schema_diff_test.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\ndescribe \"actual_db_schema:diff_schema_with_migrations\" do\n  let(:utils) { TestUtils.new }\n\n  before do\n    utils.reset_database_yml(TestingState.db_config[\"primary\"])\n    ActiveRecord::Base.configurations = { \"test\" => TestingState.db_config[\"primary\"] }\n    ActiveRecord::Tasks::DatabaseTasks.database_configuration = { \"test\" => TestingState.db_config[\"primary\"] }\n    ActiveRecord::Base.establish_connection(**TestingState.db_config[\"primary\"])\n    utils.cleanup\n\n    utils.define_migration_file(\"20250124084321_create_users.rb\", <<~RUBY)\n      class CreateUsers < ActiveRecord::Migration[6.0]\n        def change\n          create_table :users do |t|\n            t.string :name\n            t.string :middle_name\n            t.timestamps\n          end\n\n          add_index :users, :name, name: \"index_users_on_name\", unique: true\n        end\n      end\n    RUBY\n    utils.define_migration_file(\"20250124084322_create_products.rb\", <<~RUBY)\n      class CreateProducts < ActiveRecord::Migration[6.0]\n        def change\n          create_table :products do |t|\n            t.string :name\n            t.decimal :price, precision: 10, scale: 2\n            t.timestamps\n          end\n        end\n      end\n    RUBY\n    utils.run_migrations\n  end\n\n  after do\n    utils.define_migration_file(\"20250124084323_drop_users.rb\", <<~RUBY)\n      class DropUsers < ActiveRecord::Migration[6.0]\n        def change\n          drop_table :users\n        end\n      end\n    RUBY\n    utils.define_migration_file(\"20250124084324_drop_products.rb\", <<~RUBY)\n      class DropProducts < ActiveRecord::Migration[6.0]\n        def change\n          drop_table :products, if_exists: true\n        end\n      end\n    RUBY\n    utils.run_migrations\n  end\n\n  def migration_path(file_name)\n    File.join(\"test/dummy_app/db/migrate\", file_name)\n  end\n\n  def invoke_rake_task(schema_path)\n    Rake::Task[\"actual_db_schema:diff_schema_with_migrations\"].invoke(schema_path, \"test/dummy_app/db/migrate\")\n    Rake::Task[\"actual_db_schema:diff_schema_with_migrations\"].reenable\n  end\n\n  def run_migration(file_name, content)\n    utils.define_migration_file(file_name, content)\n    utils.run_migrations\n    dump_schema\n  end\n\n  def dump_schema\n    return unless Rails.configuration.active_record.schema_format == :sql\n\n    config = if ActiveRecord::Base.respond_to?(:connection_db_config)\n               ActiveRecord::Base.connection_db_config\n             else\n               ActiveRecord::Base.configurations[Rails.env]\n             end\n    ActiveRecord::Tasks::DatabaseTasks.structure_dump(config, Rails.root.join(\"db\", \"structure.sql\").to_s)\n  end\n\n  describe \"when using schema.rb\" do\n    before do\n      old_schema_content = File.read(\"test/dummy_app/db/schema.rb\")\n      ActualDbSchema::SchemaDiff.define_method(:old_schema_content) { old_schema_content }\n    end\n\n    it \"annotates adding a column\" do\n      file_name = \"20250124084325_add_surname_to_users.rb\"\n      run_migration(file_name, add_surname_to_users_migration)\n      invoke_rake_task(\"test/dummy_app/db/schema.rb\")\n      assert_match(\n        %r{\\+    t\\.string \"surname\" // #{migration_path(file_name)} //},\n        TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n      )\n    end\n\n    it \"annotates removing a column\" do\n      file_name = \"20250124084326_remove_middle_name_from_users.rb\"\n      run_migration(file_name, remove_middle_name_from_users_migration)\n      invoke_rake_task(\"test/dummy_app/db/schema.rb\")\n      assert_match(\n        %r{-    t\\.string \"middle_name\" // #{migration_path(file_name)} //},\n        TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n      )\n    end\n\n    it \"annotates changing a column\" do\n      file_name = \"20250124084327_change_price_precision_in_products.rb\"\n      run_migration(file_name, change_price_precision_in_products_migration)\n      invoke_rake_task(\"test/dummy_app/db/schema.rb\")\n      assert_match(\n        %r{-    t\\.decimal \"price\", precision: 10, scale: 2 // #{migration_path(file_name)} //},\n        TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n      )\n      assert_match(\n        %r{\\+    t\\.decimal \"price\", precision: 15, scale: 2 // #{migration_path(file_name)} //},\n        TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n      )\n    end\n\n    it \"annotates renaming a column\" do\n      file_name = \"20250124084328_rename_name_to_full_name_in_users.rb\"\n      run_migration(file_name, rename_name_to_full_name_in_users_migration)\n      invoke_rake_task(\"test/dummy_app/db/schema.rb\")\n      assert_match(\n        %r{-    t\\.string \"name\" // #{migration_path(file_name)} //},\n        TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n      )\n      assert_match(\n        %r{\\+    t\\.string \"full_name\" // #{migration_path(file_name)} //},\n        TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n      )\n    end\n\n    it \"annotates adding an index\" do\n      file_name = \"20250124084329_add_index_on_users_middle_name.rb\"\n      run_migration(file_name, add_index_on_users_middle_name_migration)\n      invoke_rake_task(\"test/dummy_app/db/schema.rb\")\n      assert_match(\n        %r{\\+    t\\.index \\[\"middle_name\"\\], name: \"index_users_on_middle_name\", unique: true // #{migration_path(file_name)} //}, # rubocop:disable Layout/LineLength\n        TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n      )\n    end\n\n    it \"annotates removing an index\" do\n      file_name = \"20250124084330_remove_index_on_users_name.rb\"\n      run_migration(file_name, remove_index_on_users_name_migration)\n      invoke_rake_task(\"test/dummy_app/db/schema.rb\")\n      assert_match(\n        %r{-    t\\.index \\[\"name\"\\], name: \"index_users_on_name\", unique: true // #{migration_path(file_name)} //},\n        TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n      )\n    end\n\n    it \"annotates renaming an index\" do\n      file_name = \"20250124084331_rename_index_on_users_name.rb\"\n      run_migration(file_name, rename_index_on_users_name_migration)\n      invoke_rake_task(\"test/dummy_app/db/schema.rb\")\n      assert_match(\n        %r{-    t\\.index \\[\"name\"\\], name: \"index_users_on_name\", unique: true // #{migration_path(file_name)} //},\n        TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n      )\n      assert_match(\n        %r{\\+    t\\.index \\[\"name\"\\], name: \"index_users_on_user_name\", unique: true // #{migration_path(file_name)} //}, # rubocop:disable Layout/LineLength\n        TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n      )\n    end\n\n    it \"annotates creating a new table\" do\n      file_name = \"20250124084332_create_categories.rb\"\n      run_migration(file_name, create_categories_migration)\n      invoke_rake_task(\"test/dummy_app/db/schema.rb\")\n      assert_match(\n        %r{\\+    create_table \"categories\", force: :cascade do |t| // #{migration_path(file_name)} //},\n        TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n      )\n      run_migration(\"20250124084333_drop_categories.rb\", drop_categories_migration)\n    end\n\n    it \"annotates dropping a table\" do\n      file_name = \"20250124084334_drop_products_table.rb\"\n      run_migration(file_name, drop_products_table_migration)\n      invoke_rake_task(\"test/dummy_app/db/schema.rb\")\n      assert_match(\n        %r{-    create_table \"products\", force: :cascade do |t| // #{migration_path(file_name)} //},\n        TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n      )\n    end\n\n    it \"processes phantom migrations from tmp/migrated folders\" do\n      file_name = \"20250124084335_phantom.rb\"\n      run_migration(file_name, phantom_migration)\n      utils.remove_app_dir(Rails.root.join(\"db\", \"migrate\", file_name))\n      utils.run_migrations\n      invoke_rake_task(\"test/dummy_app/db/schema.rb\")\n      assert_match(\n        %r{\\+    t\\.string \"email\" // #{File.join(\"test/dummy_app/tmp/migrated\", file_name)} //},\n        TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n      )\n    end\n  end\n\n  describe \"when using structure.sql\" do\n    before do\n      skip unless TestingState.db_config[\"primary\"][\"adapter\"] == \"postgresql\"\n\n      Rails.application.configure { config.active_record.schema_format = :sql }\n      dump_schema\n      old_schema_content = File.read(\"test/dummy_app/db/structure.sql\")\n      ActualDbSchema::SchemaDiff.define_method(:old_schema_content) { old_schema_content }\n    end\n\n    after do\n      Rails.application.configure { config.active_record.schema_format = :ruby }\n    end\n\n    it \"annotates adding a column\" do\n      file_name = \"20250124084325_add_surname_to_users.rb\"\n      run_migration(file_name, add_surname_to_users_migration)\n      invoke_rake_task(\"test/dummy_app/db/structure.sql\")\n      assert_match(\n        %r{\\+    surname character varying // #{migration_path(file_name)} //},\n        TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n      )\n    end\n\n    it \"annotates removing a column\" do\n      file_name = \"20250124084326_remove_middle_name_from_users.rb\"\n      run_migration(file_name, remove_middle_name_from_users_migration)\n      invoke_rake_task(\"test/dummy_app/db/structure.sql\")\n      assert_match(\n        %r{-    middle_name character varying, // #{migration_path(file_name)} //},\n        TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n      )\n    end\n\n    it \"annotates changing a column\" do\n      file_name = \"20250124084327_change_price_precision_in_products.rb\"\n      run_migration(file_name, change_price_precision_in_products_migration)\n      invoke_rake_task(\"test/dummy_app/db/structure.sql\")\n      assert_match(\n        %r{-    price numeric\\(10,2\\), // #{migration_path(file_name)} //}, TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n      )\n      assert_match(\n        %r{\\+    price numeric\\(15,2\\), // #{migration_path(file_name)} //}, TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n      )\n    end\n\n    it \"annotates renaming a column\" do\n      file_name = \"20250124084328_rename_name_to_full_name_in_users.rb\"\n      run_migration(file_name, rename_name_to_full_name_in_users_migration)\n      invoke_rake_task(\"test/dummy_app/db/structure.sql\")\n      assert_match(\n        %r{-    name character varying, // #{migration_path(file_name)} //},\n        TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n      )\n      assert_match(\n        %r{\\+    full_name character varying, // #{migration_path(file_name)} //},\n        TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n      )\n    end\n\n    it \"annotates adding an index\" do\n      file_name = \"20250124084329_add_index_on_users_middle_name.rb\"\n      run_migration(file_name, add_index_on_users_middle_name_migration)\n      invoke_rake_task(\"test/dummy_app/db/structure.sql\")\n      assert_match(\n        %r{\\+CREATE UNIQUE INDEX index_users_on_middle_name ON public.users USING btree \\(middle_name\\); // #{migration_path(file_name)} //}, # rubocop:disable Layout/LineLength\n        TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n      )\n    end\n\n    it \"annotates removing an index\" do\n      file_name = \"20250124084330_remove_index_on_users_name.rb\"\n      run_migration(file_name, remove_index_on_users_name_migration)\n      invoke_rake_task(\"test/dummy_app/db/structure.sql\")\n      assert_match(\n        %r{-CREATE UNIQUE INDEX index_users_on_name ON public.users USING btree \\(name\\); // #{migration_path(file_name)} //}, # rubocop:disable Layout/LineLength\n        TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n      )\n    end\n\n    it \"annotates renaming an index\" do\n      file_name = \"20250124084331_rename_index_on_users_name.rb\"\n      run_migration(file_name, rename_index_on_users_name_migration)\n      invoke_rake_task(\"test/dummy_app/db/structure.sql\")\n      assert_match(\n        %r{-CREATE UNIQUE INDEX index_users_on_name ON public.users USING btree \\(name\\); // #{migration_path(file_name)} //}, # rubocop:disable Layout/LineLength\n        TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n      )\n      assert_match(\n        %r{\\+CREATE UNIQUE INDEX index_users_on_user_name ON public.users USING btree \\(name\\); // #{migration_path(file_name)} //}, # rubocop:disable Layout/LineLength\n        TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n      )\n    end\n\n    it \"annotates creating a new table\" do\n      file_name = \"20250124084332_create_categories.rb\"\n      run_migration(file_name, create_categories_migration)\n      invoke_rake_task(\"test/dummy_app/db/structure.sql\")\n      assert_match(\n        %r{\\+CREATE TABLE public.categories \\( // #{migration_path(file_name)} //},\n        TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n      )\n      assert_match(\n        %r{\\+CREATE SEQUENCE public.categories_id_seq // #{migration_path(file_name)} //},\n        TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n      )\n      assert_match(\n        %r{\\+ALTER SEQUENCE public.categories_id_seq OWNED BY public.categories.id; // #{migration_path(file_name)} //},\n        TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n      )\n      assert_match(\n        %r{\\+ALTER TABLE ONLY public.categories ALTER COLUMN id SET DEFAULT nextval\\('public.categories_id_seq'::regclass\\); // #{migration_path(file_name)} //}, # rubocop:disable Layout/LineLength\n        TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n      )\n      assert_match(\n        %r{\\+ALTER TABLE ONLY public.categories // #{migration_path(file_name)} //},\n        TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n      )\n      run_migration(\"20250124084333_drop_categories.rb\", drop_categories_migration)\n    end\n\n    it \"annotates dropping a table\" do\n      file_name = \"20250124084334_drop_products_table.rb\"\n      run_migration(file_name, drop_products_table_migration)\n      invoke_rake_task(\"test/dummy_app/db/structure.sql\")\n      assert_match(\n        %r{-CREATE TABLE public.products \\( // #{migration_path(file_name)} //},\n        TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n      )\n      assert_match(\n        %r{-CREATE SEQUENCE public.products_id_seq // #{migration_path(file_name)} //},\n        TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n      )\n      assert_match(\n        %r{-ALTER SEQUENCE public.products_id_seq OWNED BY public.products.id; // #{migration_path(file_name)} //},\n        TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n      )\n      assert_match(\n        %r{-ALTER TABLE ONLY public.products ALTER COLUMN id SET DEFAULT nextval\\('public.products_id_seq'::regclass\\); // #{migration_path(file_name)} //}, # rubocop:disable Layout/LineLength\n        TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n      )\n      assert_match(\n        %r{-ALTER TABLE ONLY public.products // #{migration_path(file_name)} //},\n        TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n      )\n    end\n\n    it \"processes phantom migrations from tmp/migrated folders\" do\n      file_name = \"20250124084335_phantom.rb\"\n      run_migration(file_name, phantom_migration)\n      utils.remove_app_dir(Rails.root.join(\"db\", \"migrate\", file_name))\n      utils.run_migrations\n      dump_schema\n      invoke_rake_task(\"test/dummy_app/db/structure.sql\")\n      assert_match(\n        %r{\\+    email character varying // #{File.join(\"test/dummy_app/tmp/migrated\", file_name)} //},\n        TestingState.output.gsub(/\\e\\[\\d+m/, \"\")\n      )\n    end\n  end\n\n  def add_surname_to_users_migration\n    <<~RUBY\n      class AddSurnameToUsers < ActiveRecord::Migration[6.0]\n        def change\n          add_column :users, :surname, :string\n        end\n      end\n    RUBY\n  end\n\n  def remove_middle_name_from_users_migration\n    <<~RUBY\n      class RemoveMiddleNameFromUsers < ActiveRecord::Migration[6.0]\n        def change\n          remove_column :users, :middle_name\n        end\n      end\n    RUBY\n  end\n\n  def change_price_precision_in_products_migration\n    <<~RUBY\n      class ChangePricePrecisionInProducts < ActiveRecord::Migration[6.0]\n        def change\n          change_column :products, :price, :decimal, precision: 15, scale: 2\n        end\n      end\n    RUBY\n  end\n\n  def rename_name_to_full_name_in_users_migration\n    <<~RUBY\n      class RenameNameToFullNameInUsers < ActiveRecord::Migration[6.0]\n        def change\n          rename_column :users, :name, :full_name\n        end\n      end\n    RUBY\n  end\n\n  def add_index_on_users_middle_name_migration\n    <<~RUBY\n      class AddIndexOnUsersMiddleName < ActiveRecord::Migration[6.0]\n        def change\n          add_index :users, :middle_name, name: \"index_users_on_middle_name\", unique: true\n        end\n      end\n    RUBY\n  end\n\n  def remove_index_on_users_name_migration\n    <<~RUBY\n      class RemoveIndexOnUsersName < ActiveRecord::Migration[6.0]\n        def change\n          remove_index :users, name: \"index_users_on_name\"\n        end\n      end\n    RUBY\n  end\n\n  def rename_index_on_users_name_migration\n    <<~RUBY\n      class RenameIndexOnUsersName < ActiveRecord::Migration[6.0]\n        def change\n          rename_index :users, \"index_users_on_name\", \"index_users_on_user_name\"\n        end\n      end\n    RUBY\n  end\n\n  def create_categories_migration\n    <<~RUBY\n      class CreateCategories < ActiveRecord::Migration[6.0]\n        def change\n          create_table :categories do |t|\n            t.string :title\n            t.timestamps\n          end\n        end\n      end\n    RUBY\n  end\n\n  def drop_categories_migration\n    <<~RUBY\n      class DropCategories < ActiveRecord::Migration[6.0]\n        def change\n          drop_table :categories\n        end\n      end\n    RUBY\n  end\n\n  def drop_products_table_migration\n    <<~RUBY\n      class DropProductsTable < ActiveRecord::Migration[6.0]\n        def change\n          drop_table :products\n        end\n      end\n    RUBY\n  end\n\n  def phantom_migration\n    <<~RUBY\n      class Phantom < ActiveRecord::Migration[6.0]\n        disable_ddl_transaction!\n\n        def up\n          TestingState.up << :phantom\n        end\n\n        def down\n          add_column :users, :email, :string\n          raise ActiveRecord::IrreversibleMigration\n        end\n      end\n    RUBY\n  end\nend\n"
  },
  {
    "path": "test/rake_task_secondary_db_storage_test.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\ndescribe \"second db support (db storage)\" do\n  let(:utils) do\n    TestUtils.new(migrations_path: \"db/migrate_secondary\", migrated_path: \"tmp/migrated_migrate_secondary\")\n  end\n\n  before do\n    ActualDbSchema.config[:migrations_storage] = :db\n    utils.reset_database_yml(TestingState.db_config[\"secondary\"])\n    ActiveRecord::Base.configurations = { \"test\" => TestingState.db_config[\"secondary\"] }\n    ActiveRecord::Tasks::DatabaseTasks.database_configuration = { \"test\" => TestingState.db_config[\"secondary\"] }\n    ActiveRecord::Base.establish_connection(**TestingState.db_config[\"secondary\"])\n    utils.clear_db_storage_table\n    utils.cleanup\n  end\n\n  describe \"db:rollback_branches\" do\n    it \"creates the tmp/migrated_migrate_secondary folder\" do\n      refute File.exist?(utils.app_file(\"tmp/migrated_migrate_secondary\"))\n      utils.run_migrations\n      assert File.exist?(utils.app_file(\"tmp/migrated_migrate_secondary\"))\n    end\n\n    it \"migrates the migrations\" do\n      assert_empty utils.applied_migrations\n      utils.run_migrations\n      assert_equal %w[20130906111511 20130906111512], utils.applied_migrations\n    end\n\n    it \"keeps migrated migrations in tmp/migrated folder\" do\n      utils.run_migrations\n      assert_equal %w[20130906111511_first.rb 20130906111512_second.rb], utils.migrated_files\n    end\n\n    it \"rolls back the migrations in the reversed order\" do\n      utils.prepare_phantom_migrations\n      assert_empty TestingState.down\n      utils.run_migrations\n      assert_equal %i[second first], TestingState.down\n      assert_empty utils.migrated_files\n    end\n\n    describe \"with irreversible migration\" do\n      before do\n        utils.define_migration_file(\"20130906111513_irreversible.rb\", <<~RUBY)\n          class Irreversible < ActiveRecord::Migration[6.0]\n            def up\n              TestingState.up << :irreversible\n            end\n\n            def down\n              raise ActiveRecord::IrreversibleMigration\n            end\n          end\n        RUBY\n      end\n\n      it \"keeps track of the irreversible migrations\" do\n        utils.prepare_phantom_migrations\n        assert_equal %i[first second irreversible], TestingState.up\n        assert_empty ActualDbSchema.failed\n        utils.run_migrations\n        assert_equal(%w[20130906111513_irreversible.rb], ActualDbSchema.failed.map { |m| File.basename(m.filename) })\n        assert_equal %w[20130906111513_irreversible.rb], utils.migrated_files\n      end\n    end\n  end\n\n  describe \"db:rollback_branches:manual\" do\n    it \"rolls back the migrations in the reversed order\" do\n      utils.prepare_phantom_migrations\n      assert_equal %i[first second], TestingState.up\n      assert_empty TestingState.down\n      assert_empty ActualDbSchema.failed\n      utils.simulate_input(\"y\") do\n        Rake::Task[\"db:rollback_branches:manual\"].invoke\n        Rake::Task[\"db:rollback_branches:manual\"].reenable\n      end\n      assert_equal %i[second first], TestingState.down\n      assert_empty utils.migrated_files\n    end\n\n    it \"skips migrations if the input is 'n'\" do\n      utils.prepare_phantom_migrations\n      assert_equal %i[first second], TestingState.up\n      assert_empty TestingState.down\n      assert_empty ActualDbSchema.failed\n\n      utils.simulate_input(\"n\") do\n        Rake::Task[\"db:rollback_branches:manual\"].invoke\n        Rake::Task[\"db:rollback_branches:manual\"].reenable\n      end\n      assert_empty TestingState.down\n      assert_equal %i[first second], TestingState.up\n      assert_equal %w[20130906111511_first.rb 20130906111512_second.rb], utils.migrated_files\n    end\n\n    describe \"with irreversible migration\" do\n      before do\n        utils.define_migration_file(\"20130906111513_irreversible.rb\", <<~RUBY)\n          class Irreversible < ActiveRecord::Migration[6.0]\n            def up\n              TestingState.up << :irreversible\n            end\n\n            def down\n              raise ActiveRecord::IrreversibleMigration\n            end\n          end\n        RUBY\n      end\n\n      it \"keeps track of the irreversible migrations\" do\n        utils.prepare_phantom_migrations\n        assert_equal %i[first second irreversible], TestingState.up\n        assert_empty ActualDbSchema.failed\n        utils.simulate_input(\"y\") do\n          Rake::Task[\"db:rollback_branches:manual\"].invoke\n          Rake::Task[\"db:rollback_branches:manual\"].reenable\n        end\n        assert_equal %i[second first], TestingState.down\n        assert_equal(%w[20130906111513_irreversible.rb], ActualDbSchema.failed.map { |m| File.basename(m.filename) })\n        assert_equal %w[20130906111513_irreversible.rb], utils.migrated_files\n      end\n    end\n  end\n\n  describe \"db:phantom_migrations\" do\n    it \"shows the list of phantom migrations\" do\n      ActualDbSchema::Git.stub(:current_branch, \"fix-bug\") do\n        utils.prepare_phantom_migrations\n        Rake::Task[\"db:phantom_migrations\"].invoke\n        Rake::Task[\"db:phantom_migrations\"].reenable\n        assert_match(/ Status   Migration ID    Branch   Migration File/, TestingState.output)\n        assert_match(/---------------------------------------------------/, TestingState.output)\n        assert_match(\n          %r{   up     20130906111511  fix-bug  tmp/migrated_migrate_secondary/20130906111511_first.rb},\n          TestingState.output\n        )\n        assert_match(\n          %r{   up     20130906111512  fix-bug  tmp/migrated_migrate_secondary/20130906111512_second.rb},\n          TestingState.output\n        )\n      end\n    end\n  end\n\n  after do\n    utils.clear_db_storage_table\n    ActualDbSchema.config[:migrations_storage] = :file\n  end\nend\n"
  },
  {
    "path": "test/rake_task_secondary_test.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\ndescribe \"second db support\" do\n  let(:utils) do\n    TestUtils.new(migrations_path: \"db/migrate_secondary\", migrated_path: \"tmp/migrated_migrate_secondary\")\n  end\n\n  before do\n    utils.reset_database_yml(TestingState.db_config[\"secondary\"])\n    ActiveRecord::Base.configurations = { \"test\" => TestingState.db_config[\"secondary\"] }\n    ActiveRecord::Tasks::DatabaseTasks.database_configuration = { \"test\" => TestingState.db_config[\"secondary\"] }\n    ActiveRecord::Base.establish_connection(**TestingState.db_config[\"secondary\"])\n    utils.cleanup\n  end\n\n  describe \"db:rollback_branches\" do\n    it \"creates the tmp/migrated_migrate_secondary folder\" do\n      refute File.exist?(utils.app_file(\"tmp/migrated_migrate_secondary\"))\n      utils.run_migrations\n      assert File.exist?(utils.app_file(\"tmp/migrated_migrate_secondary\"))\n    end\n\n    it \"migrates the migrations\" do\n      assert_empty utils.applied_migrations\n      utils.run_migrations\n      assert_equal %w[20130906111511 20130906111512], utils.applied_migrations\n    end\n\n    it \"keeps migrated migrations in tmp/migrated folder\" do\n      utils.run_migrations\n      assert_equal %w[20130906111511_first.rb 20130906111512_second.rb], utils.migrated_files\n    end\n\n    it \"rolls back the migrations in the reversed order\" do\n      utils.prepare_phantom_migrations\n      assert_empty TestingState.down\n      utils.run_migrations\n      assert_equal %i[second first], TestingState.down\n      assert_empty utils.migrated_files\n    end\n\n    describe \"with irreversible migration\" do\n      before do\n        utils.define_migration_file(\"20130906111513_irreversible.rb\", <<~RUBY)\n          class Irreversible < ActiveRecord::Migration[6.0]\n            def up\n              TestingState.up << :irreversible\n            end\n\n            def down\n              raise ActiveRecord::IrreversibleMigration\n            end\n          end\n        RUBY\n      end\n\n      it \"keeps track of the irreversible migrations\" do\n        utils.prepare_phantom_migrations\n        assert_equal %i[first second irreversible], TestingState.up\n        assert_empty ActualDbSchema.failed\n        utils.run_migrations\n        assert_equal(%w[20130906111513_irreversible.rb], ActualDbSchema.failed.map { |m| File.basename(m.filename) })\n        assert_equal %w[20130906111513_irreversible.rb], utils.migrated_files\n      end\n    end\n  end\n\n  describe \"db:rollback_branches:manual\" do\n    it \"rolls back the migrations in the reversed order\" do\n      utils.prepare_phantom_migrations\n      assert_equal %i[first second], TestingState.up\n      assert_empty TestingState.down\n      assert_empty ActualDbSchema.failed\n      utils.simulate_input(\"y\") do\n        Rake::Task[\"db:rollback_branches:manual\"].invoke\n        Rake::Task[\"db:rollback_branches:manual\"].reenable\n      end\n      assert_equal %i[second first], TestingState.down\n      assert_empty utils.migrated_files\n    end\n\n    it \"skips migrations if the input is 'n'\" do\n      utils.prepare_phantom_migrations\n      assert_equal %i[first second], TestingState.up\n      assert_empty TestingState.down\n      assert_empty ActualDbSchema.failed\n\n      utils.simulate_input(\"n\") do\n        Rake::Task[\"db:rollback_branches:manual\"].invoke\n        Rake::Task[\"db:rollback_branches:manual\"].reenable\n      end\n      assert_empty TestingState.down\n      assert_equal %i[first second], TestingState.up\n      assert_equal %w[20130906111511_first.rb 20130906111512_second.rb], utils.migrated_files\n    end\n\n    describe \"with irreversible migration\" do\n      before do\n        utils.define_migration_file(\"20130906111513_irreversible.rb\", <<~RUBY)\n          class Irreversible < ActiveRecord::Migration[6.0]\n            def up\n              TestingState.up << :irreversible\n            end\n\n            def down\n              raise ActiveRecord::IrreversibleMigration\n            end\n          end\n        RUBY\n      end\n\n      it \"keeps track of the irreversible migrations\" do\n        utils.prepare_phantom_migrations\n        assert_equal %i[first second irreversible], TestingState.up\n        assert_empty ActualDbSchema.failed\n        utils.simulate_input(\"y\") do\n          Rake::Task[\"db:rollback_branches:manual\"].invoke\n          Rake::Task[\"db:rollback_branches:manual\"].reenable\n        end\n        assert_equal %i[second first], TestingState.down\n        assert_equal(%w[20130906111513_irreversible.rb], ActualDbSchema.failed.map { |m| File.basename(m.filename) })\n        assert_equal %w[20130906111513_irreversible.rb], utils.migrated_files\n      end\n    end\n  end\n\n  describe \"db:phantom_migrations\" do\n    it \"shows the list of phantom migrations\" do\n      ActualDbSchema::Git.stub(:current_branch, \"fix-bug\") do\n        utils.prepare_phantom_migrations\n        Rake::Task[\"db:phantom_migrations\"].invoke\n        Rake::Task[\"db:phantom_migrations\"].reenable\n        assert_match(/ Status   Migration ID    Branch   Migration File/, TestingState.output)\n        assert_match(/---------------------------------------------------/, TestingState.output)\n        assert_match(\n          %r{   up     20130906111511  fix-bug  tmp/migrated_migrate_secondary/20130906111511_first.rb},\n          TestingState.output\n        )\n        assert_match(\n          %r{   up     20130906111512  fix-bug  tmp/migrated_migrate_secondary/20130906111512_second.rb},\n          TestingState.output\n        )\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "test/rake_task_test.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\ndescribe \"single db\" do\n  let(:utils) { TestUtils.new }\n\n  before do\n    utils.reset_database_yml(TestingState.db_config[\"primary\"])\n    ActiveRecord::Base.configurations = { \"test\" => TestingState.db_config[\"primary\"] }\n    ActiveRecord::Tasks::DatabaseTasks.database_configuration = { \"test\" => TestingState.db_config[\"primary\"] }\n    ActiveRecord::Base.establish_connection(**TestingState.db_config[\"primary\"])\n    utils.cleanup\n  end\n\n  describe \"db:rollback_branches\" do\n    def collect_rollback_events\n      events = []\n      subscriber = ActiveSupport::Notifications.subscribe(ActualDbSchema::Instrumentation::ROLLBACK_EVENT) do |*args|\n        events << ActiveSupport::Notifications::Event.new(*args)\n      end\n\n      yield events\n    ensure\n      ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber\n    end\n\n    it \"creates the tmp/migrated folder\" do\n      refute File.exist?(utils.app_file(\"tmp/migrated\"))\n      utils.run_migrations\n      assert File.exist?(utils.app_file(\"tmp/migrated\"))\n    end\n\n    it \"migrates the migrations\" do\n      assert_empty utils.applied_migrations\n      utils.run_migrations\n      assert_equal %w[20130906111511 20130906111512], utils.applied_migrations\n    end\n\n    it \"keeps migrated migrations in tmp/migrated folder\" do\n      utils.run_migrations\n      assert_equal %w[20130906111511_first.rb 20130906111512_second.rb], utils.migrated_files\n    end\n\n    it \"rolls back the migrations in the reversed order\" do\n      utils.prepare_phantom_migrations\n      assert_empty TestingState.down\n      utils.run_migrations\n      assert_equal %i[second first], TestingState.down\n      assert_match(/\\[ActualDbSchema\\] Rolling back phantom migration/, TestingState.output)\n      assert_empty utils.migrated_files\n    end\n\n    it \"emits one instrumentation event per successful rollback\" do\n      utils.prepare_phantom_migrations\n      events = nil\n\n      collect_rollback_events do |captured_events|\n        utils.run_migrations\n        events = captured_events\n      end\n\n      assert_equal 2, events.size\n      assert_equal(%w[20130906111512 20130906111511], events.map { |event| event.payload[:version] })\n      assert_equal([false, false], events.map { |event| event.payload[:manual_mode] })\n      assert_equal([utils.primary_database, utils.primary_database], events.map { |event| event.payload[:database] })\n      assert_equal([nil, nil], events.map { |event| event.payload[:schema] })\n    end\n\n    describe \"with irreversible migration\" do\n      before do\n        utils.define_migration_file(\"20130906111513_irreversible.rb\", <<~RUBY)\n          class Irreversible < ActiveRecord::Migration[6.0]\n            def up\n              TestingState.up << :irreversible\n            end\n\n            def down\n              raise ActiveRecord::IrreversibleMigration\n            end\n          end\n        RUBY\n      end\n\n      it \"keeps track of the irreversible migrations\" do\n        utils.prepare_phantom_migrations\n        assert_equal %i[first second irreversible], TestingState.up\n        assert_empty ActualDbSchema.failed\n        utils.run_migrations\n        assert_equal(%w[20130906111513_irreversible.rb], ActualDbSchema.failed.map { |m| File.basename(m.filename) })\n        assert_match(/Error encountered during rollback:/, TestingState.output)\n        assert_match(/ActiveRecord::IrreversibleMigration/, TestingState.output)\n        assert_equal %w[20130906111513_irreversible.rb], utils.migrated_files\n      end\n\n      it \"does not emit instrumentation for failed rollbacks\" do\n        utils.prepare_phantom_migrations\n        events = nil\n\n        collect_rollback_events do |captured_events|\n          utils.run_migrations\n          events = captured_events\n        end\n\n        assert_equal(%w[20130906111512 20130906111511], events.map { |event| event.payload[:version] })\n      end\n    end\n\n    describe \"with irreversible migration is the first\" do\n      before do\n        utils.define_migration_file(\"20130906111510_irreversible.rb\", <<~RUBY)\n          class Irreversible < ActiveRecord::Migration[6.0]\n            def up\n              TestingState.up << :irreversible\n            end\n\n            def down\n              raise ActiveRecord::IrreversibleMigration\n            end\n          end\n        RUBY\n      end\n\n      it \"doesn't fail fast and has formatted output\" do\n        utils.prepare_phantom_migrations\n        assert_equal %i[irreversible first second], TestingState.up\n        assert_empty ActualDbSchema.failed\n        utils.run_migrations\n        assert_equal(%w[20130906111510_irreversible.rb], ActualDbSchema.failed.map { |m| File.basename(m.filename) })\n        assert_match(/1 phantom migration\\(s\\) could not be rolled back automatically/, TestingState.output)\n        assert_match(/Try these steps to fix and move forward:/, TestingState.output)\n        assert_match(/Below are the details of the problematic migrations:/, TestingState.output)\n        assert_match(%r{File: tmp/migrated/20130906111510_irreversible.rb}, TestingState.output)\n        assert_equal %w[20130906111510_irreversible.rb], utils.migrated_files\n      end\n    end\n\n    describe \"with acronyms defined\" do\n      before do\n        utils.define_migration_file(\"20241218064344_ts360.rb\", <<~RUBY)\n          class Ts360 < ActiveRecord::Migration[6.0]\n            def up\n              TestingState.up << :ts360\n            end\n\n            def down\n              TestingState.down << :ts360\n            end\n          end\n        RUBY\n      end\n\n      it \"rolls back the phantom migrations without failing\" do\n        utils.prepare_phantom_migrations\n        assert_equal %i[first second ts360], TestingState.up\n        assert_empty ActualDbSchema.failed\n        utils.define_acronym(\"TS360\")\n        utils.run_migrations\n        assert_equal %i[ts360 second first], TestingState.down\n        assert_empty ActualDbSchema.failed\n        assert_empty utils.migrated_files\n      end\n    end\n\n    describe \"with custom migrated folder\" do\n      before do\n        ActualDbSchema.configure { |config| config.migrated_folder = Rails.root.join(\"custom\", \"migrated\") }\n      end\n\n      after do\n        utils.remove_app_dir(\"custom/migrated\")\n        ActualDbSchema.configure { |config| config.migrated_folder = nil }\n      end\n\n      it \"creates the custom migrated folder\" do\n        refute File.exist?(utils.app_file(\"custom/migrated\"))\n        utils.run_migrations\n        assert File.exist?(utils.app_file(\"custom/migrated\"))\n      end\n\n      it \"keeps migrated migrations in the custom migrated folder\" do\n        utils.run_migrations\n        assert_equal %w[20130906111511_first.rb 20130906111512_second.rb], utils.migrated_files\n      end\n\n      it \"rolls back the migrations in the reversed order\" do\n        utils.prepare_phantom_migrations\n        assert_empty TestingState.down\n        utils.run_migrations\n        assert_equal %i[second first], TestingState.down\n        assert_match(/\\[ActualDbSchema\\] Rolling back phantom migration/, TestingState.output)\n        assert_empty utils.migrated_files\n      end\n    end\n\n    describe \"when app is not a git repository\" do\n      it \"doesn't show an error message\" do\n        Dir.mktmpdir do |dir|\n          Dir.chdir(dir) do\n            _out, err = capture_subprocess_io do\n              utils.prepare_phantom_migrations\n            end\n\n            refute_match(\"fatal: not a git repository\", err)\n            assert_equal \"unknown\", ActualDbSchema::Git.current_branch\n          end\n        end\n      end\n    end\n  end\n\n  describe \"db:rollback_branches:manual\" do\n    it \"rolls back the migrations in the reversed order\" do\n      utils.prepare_phantom_migrations\n      assert_equal %i[first second], TestingState.up\n      assert_empty TestingState.down\n      assert_empty ActualDbSchema.failed\n      utils.simulate_input(\"y\") do\n        Rake::Task[\"db:rollback_branches:manual\"].invoke\n        Rake::Task[\"db:rollback_branches:manual\"].reenable\n      end\n      assert_equal %i[second first], TestingState.down\n      assert_empty utils.migrated_files\n    end\n\n    it \"skips migrations if the input is 'n'\" do\n      utils.prepare_phantom_migrations\n      assert_equal %i[first second], TestingState.up\n      assert_empty TestingState.down\n      assert_empty ActualDbSchema.failed\n\n      utils.simulate_input(\"n\") do\n        Rake::Task[\"db:rollback_branches:manual\"].invoke\n        Rake::Task[\"db:rollback_branches:manual\"].reenable\n      end\n      assert_empty TestingState.down\n      assert_equal %i[first second], TestingState.up\n      assert_equal %w[20130906111511_first.rb 20130906111512_second.rb], utils.migrated_files\n    end\n\n    describe \"with irreversible migration\" do\n      before do\n        utils.define_migration_file(\"20130906111513_irreversible.rb\", <<~RUBY)\n          class Irreversible < ActiveRecord::Migration[6.0]\n            def up\n              TestingState.up << :irreversible\n            end\n\n            def down\n              raise ActiveRecord::IrreversibleMigration\n            end\n          end\n        RUBY\n      end\n\n      it \"keeps track of the irreversible migrations\" do\n        utils.prepare_phantom_migrations\n        assert_equal %i[first second irreversible], TestingState.up\n        assert_empty ActualDbSchema.failed\n        utils.simulate_input(\"y\") do\n          Rake::Task[\"db:rollback_branches:manual\"].invoke\n          Rake::Task[\"db:rollback_branches:manual\"].reenable\n        end\n        assert_equal %i[second first], TestingState.down\n        assert_equal(%w[20130906111513_irreversible.rb], ActualDbSchema.failed.map { |m| File.basename(m.filename) })\n        assert_equal %w[20130906111513_irreversible.rb], utils.migrated_files\n      end\n    end\n  end\n\n  describe \"db:phantom_migrations\" do\n    it \"shows the list of phantom migrations\" do\n      ActualDbSchema::Git.stub(:current_branch, \"fix-bug\") do\n        utils.prepare_phantom_migrations\n        Rake::Task[\"db:phantom_migrations\"].invoke\n        Rake::Task[\"db:phantom_migrations\"].reenable\n        assert_match(/ Status   Migration ID    Branch   Migration File/, TestingState.output)\n        assert_match(/---------------------------------------------------/, TestingState.output)\n        assert_match(%r{   up     20130906111511  fix-bug  tmp/migrated/20130906111511_first.rb}, TestingState.output)\n        assert_match(%r{   up     20130906111512  fix-bug  tmp/migrated/20130906111512_second.rb}, TestingState.output)\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "test/rake_tasks_all_databases_db_storage_test.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\ndescribe \"multipe db support (db storage)\" do\n  let(:utils) do\n    TestUtils.new(\n      migrations_path: [\"db/migrate\", \"db/migrate_secondary\"],\n      migrated_path: [\"tmp/migrated\", \"tmp/migrated_migrate_secondary\"]\n    )\n  end\n\n  before do\n    ActualDbSchema.config[:migrations_storage] = :db\n    utils.reset_database_yml(TestingState.db_config)\n    ActiveRecord::Base.configurations = { \"test\" => TestingState.db_config }\n    ActiveRecord::Tasks::DatabaseTasks.database_configuration = { \"test\" => TestingState.db_config }\n    utils.cleanup(TestingState.db_config)\n    utils.clear_db_storage_table(TestingState.db_config)\n  end\n\n  describe \"db:rollback_branches\" do\n    it \"creates the tmp/migrated folder\" do\n      refute File.exist?(utils.app_file(\"tmp/migrated\"))\n      refute File.exist?(utils.app_file(\"tmp/migrated_migrate_secondary\"))\n      utils.run_migrations\n      assert File.exist?(utils.app_file(\"tmp/migrated\"))\n      assert File.exist?(utils.app_file(\"tmp/migrated_migrate_secondary\"))\n    end\n\n    it \"migrates the migrations\" do\n      assert_empty utils.applied_migrations(TestingState.db_config)\n      utils.run_migrations\n      assert_equal(\n        %w[20130906111511 20130906111512 20130906111514 20130906111515],\n        utils.applied_migrations(TestingState.db_config)\n      )\n    end\n\n    it \"keeps migrated migrations in tmp/migrated folder\" do\n      utils.run_migrations\n      assert_equal(\n        %w[\n          20130906111511_first_primary.rb\n          20130906111512_second_primary.rb\n          20130906111514_first_secondary.rb\n          20130906111515_second_secondary.rb\n        ],\n        utils.migrated_files(TestingState.db_config)\n      )\n    end\n\n    it \"rolls back the migrations in the reversed order\" do\n      utils.prepare_phantom_migrations(TestingState.db_config)\n      assert_empty TestingState.down\n      utils.run_migrations\n      assert_equal %i[second_primary first_primary second_secondary first_secondary], TestingState.down\n      assert_empty utils.migrated_files(TestingState.db_config)\n    end\n\n    describe \"with irreversible migration\" do\n      before do\n        %w[primary secondary].each do |prefix|\n          utils.define_migration_file(\"20130906111513_irreversible_#{prefix}.rb\", <<~RUBY, prefix: prefix)\n            class Irreversible#{prefix.camelize} < ActiveRecord::Migration[6.0]\n              def up\n                TestingState.up << :irreversible_#{prefix}\n              end\n\n              def down\n                raise ActiveRecord::IrreversibleMigration\n              end\n            end\n          RUBY\n        end\n      end\n\n      it \"keeps track of the irreversible migrations\" do\n        utils.prepare_phantom_migrations(TestingState.db_config)\n        assert_equal(\n          %i[first_primary second_primary irreversible_primary irreversible_secondary first_secondary second_secondary],\n          TestingState.up\n        )\n        assert_empty ActualDbSchema.failed\n        utils.run_migrations\n        failed = ActualDbSchema.failed.map { |m| File.basename(m.filename) }\n        assert_equal(%w[20130906111513_irreversible_primary.rb 20130906111513_irreversible_secondary.rb], failed)\n        assert_equal(\n          %w[20130906111513_irreversible_primary.rb 20130906111513_irreversible_secondary.rb],\n          utils.migrated_files(TestingState.db_config)\n        )\n      end\n    end\n  end\n\n  describe \"db:rollback_branches:manual\" do\n    it \"skips migrations if the input is 'n'\" do\n      utils.prepare_phantom_migrations\n      assert_equal %i[first_primary second_primary first_secondary second_secondary], TestingState.up\n      assert_empty TestingState.down\n      assert_empty ActualDbSchema.failed\n\n      utils.simulate_input(\"n\") do\n        Rake::Task[\"db:rollback_branches:manual\"].invoke\n        Rake::Task[\"db:rollback_branches:manual\"].reenable\n      end\n      assert_empty TestingState.down\n      assert_equal %i[first_primary second_primary first_secondary second_secondary], TestingState.up\n      assert_equal(\n        %w[\n          20130906111511_first_primary.rb\n          20130906111512_second_primary.rb\n          20130906111514_first_secondary.rb\n          20130906111515_second_secondary.rb\n        ],\n        utils.migrated_files(TestingState.db_config)\n      )\n    end\n\n    describe \"with irreversible migration\" do\n      before do\n        %w[primary secondary].each do |prefix|\n          utils.define_migration_file(\"20130906111513_irreversible_#{prefix}.rb\", <<~RUBY, prefix: prefix)\n            class Irreversible#{prefix.camelize} < ActiveRecord::Migration[6.0]\n              def up\n                TestingState.up << :irreversible_#{prefix}\n              end\n\n              def down\n                raise ActiveRecord::IrreversibleMigration\n              end\n            end\n          RUBY\n        end\n      end\n\n      it \"keeps track of the irreversible migrations\" do\n        utils.prepare_phantom_migrations(TestingState.db_config)\n        assert_equal(\n          %i[first_primary second_primary irreversible_primary irreversible_secondary first_secondary second_secondary],\n          TestingState.up\n        )\n        assert_empty ActualDbSchema.failed\n        utils.simulate_input(\"y\") do\n          Rake::Task[\"db:rollback_branches:manual\"].invoke\n          Rake::Task[\"db:rollback_branches:manual\"].reenable\n        end\n        failed = ActualDbSchema.failed.map { |m| File.basename(m.filename) }\n        assert_equal(%w[20130906111513_irreversible_primary.rb 20130906111513_irreversible_secondary.rb], failed)\n        assert_equal(\n          %w[20130906111513_irreversible_primary.rb 20130906111513_irreversible_secondary.rb],\n          utils.migrated_files(TestingState.db_config)\n        )\n      end\n    end\n  end\n\n  describe \"db:phantom_migrations\" do\n    it \"shows the list of phantom migrations\" do\n      ActualDbSchema::Git.stub(:current_branch, \"fix-bug\") do\n        utils.prepare_phantom_migrations(TestingState.db_config)\n        Rake::Task[\"db:phantom_migrations\"].invoke\n        Rake::Task[\"db:phantom_migrations\"].reenable\n        assert_match(/ Status   Migration ID    Branch   Migration File/, TestingState.output)\n        assert_match(/---------------------------------------------------/, TestingState.output)\n        assert_match(\n          %r{   up     20130906111511  fix-bug  tmp/migrated/20130906111511_first_primary.rb},\n          TestingState.output\n        )\n        assert_match(\n          %r{   up     20130906111512  fix-bug  tmp/migrated/20130906111512_second_primary.rb},\n          TestingState.output\n        )\n        assert_match(\n          %r{   up     20130906111514  fix-bug  tmp/migrated_migrate_secondary/20130906111514_first_secondary.rb},\n          TestingState.output\n        )\n        assert_match(\n          %r{   up     20130906111515  fix-bug  tmp/migrated_migrate_secondary/20130906111515_second_secondary.rb},\n          TestingState.output\n        )\n      end\n    end\n  end\n\n  after do\n    utils.clear_db_storage_table(TestingState.db_config)\n    ActualDbSchema.config[:migrations_storage] = :file\n  end\nend\n"
  },
  {
    "path": "test/rake_tasks_all_databases_test.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\ndescribe \"multipe db support\" do\n  let(:utils) do\n    TestUtils.new(\n      migrations_path: [\"db/migrate\", \"db/migrate_secondary\"],\n      migrated_path: [\"tmp/migrated\", \"tmp/migrated_migrate_secondary\"]\n    )\n  end\n\n  before do\n    utils.reset_database_yml(TestingState.db_config)\n    ActiveRecord::Base.configurations = { \"test\" => TestingState.db_config }\n    ActiveRecord::Tasks::DatabaseTasks.database_configuration = { \"test\" => TestingState.db_config }\n    utils.cleanup(TestingState.db_config)\n  end\n\n  describe \"db:rollback_branches\" do\n    it \"creates the tmp/migrated folder\" do\n      refute File.exist?(utils.app_file(\"tmp/migrated\"))\n      refute File.exist?(utils.app_file(\"tmp/migrated_migrate_secondary\"))\n      utils.run_migrations\n      assert File.exist?(utils.app_file(\"tmp/migrated\"))\n      assert File.exist?(utils.app_file(\"tmp/migrated_migrate_secondary\"))\n    end\n\n    it \"migrates the migrations\" do\n      assert_empty utils.applied_migrations(TestingState.db_config)\n      utils.run_migrations\n      assert_equal(\n        %w[20130906111511 20130906111512 20130906111514 20130906111515],\n        utils.applied_migrations(TestingState.db_config)\n      )\n    end\n\n    it \"keeps migrated migrations in tmp/migrated folder\" do\n      utils.run_migrations\n      assert_equal(\n        %w[\n          20130906111511_first_primary.rb\n          20130906111512_second_primary.rb\n          20130906111514_first_secondary.rb\n          20130906111515_second_secondary.rb\n        ],\n        utils.migrated_files(TestingState.db_config)\n      )\n    end\n\n    it \"rolls back the migrations in the reversed order\" do\n      utils.prepare_phantom_migrations(TestingState.db_config)\n      assert_empty TestingState.down\n      utils.run_migrations\n      assert_equal %i[second_primary first_primary second_secondary first_secondary], TestingState.down\n      assert_empty utils.migrated_files(TestingState.db_config)\n    end\n\n    describe \"with irreversible migration\" do\n      before do\n        %w[primary secondary].each do |prefix|\n          utils.define_migration_file(\"20130906111513_irreversible_#{prefix}.rb\", <<~RUBY, prefix: prefix)\n            class Irreversible#{prefix.camelize} < ActiveRecord::Migration[6.0]\n              def up\n                TestingState.up << :irreversible_#{prefix}\n              end\n\n              def down\n                raise ActiveRecord::IrreversibleMigration\n              end\n            end\n          RUBY\n        end\n      end\n\n      it \"keeps track of the irreversible migrations\" do\n        utils.prepare_phantom_migrations(TestingState.db_config)\n        assert_equal(\n          %i[first_primary second_primary irreversible_primary irreversible_secondary first_secondary second_secondary],\n          TestingState.up\n        )\n        assert_empty ActualDbSchema.failed\n        utils.run_migrations\n        failed = ActualDbSchema.failed.map { |m| File.basename(m.filename) }\n        assert_equal(%w[20130906111513_irreversible_primary.rb 20130906111513_irreversible_secondary.rb], failed)\n        assert_equal(\n          %w[20130906111513_irreversible_primary.rb 20130906111513_irreversible_secondary.rb],\n          utils.migrated_files(TestingState.db_config)\n        )\n      end\n    end\n  end\n\n  describe \"db:rollback_branches:manual\" do\n    it \"skips migrations if the input is 'n'\" do\n      utils.prepare_phantom_migrations\n      assert_equal %i[first_primary second_primary first_secondary second_secondary], TestingState.up\n      assert_empty TestingState.down\n      assert_empty ActualDbSchema.failed\n\n      utils.simulate_input(\"n\") do\n        Rake::Task[\"db:rollback_branches:manual\"].invoke\n        Rake::Task[\"db:rollback_branches:manual\"].reenable\n      end\n      assert_empty TestingState.down\n      assert_equal %i[first_primary second_primary first_secondary second_secondary], TestingState.up\n      assert_equal(\n        %w[\n          20130906111511_first_primary.rb\n          20130906111512_second_primary.rb\n          20130906111514_first_secondary.rb\n          20130906111515_second_secondary.rb\n        ],\n        utils.migrated_files(TestingState.db_config)\n      )\n    end\n\n    describe \"with irreversible migration\" do\n      before do\n        %w[primary secondary].each do |prefix|\n          utils.define_migration_file(\"20130906111513_irreversible_#{prefix}.rb\", <<~RUBY, prefix: prefix)\n            class Irreversible#{prefix.camelize} < ActiveRecord::Migration[6.0]\n              def up\n                TestingState.up << :irreversible_#{prefix}\n              end\n\n              def down\n                raise ActiveRecord::IrreversibleMigration\n              end\n            end\n          RUBY\n        end\n      end\n\n      it \"keeps track of the irreversible migrations\" do\n        utils.prepare_phantom_migrations(TestingState.db_config)\n        assert_equal(\n          %i[first_primary second_primary irreversible_primary irreversible_secondary first_secondary second_secondary],\n          TestingState.up\n        )\n        assert_empty ActualDbSchema.failed\n        utils.simulate_input(\"y\") do\n          Rake::Task[\"db:rollback_branches:manual\"].invoke\n          Rake::Task[\"db:rollback_branches:manual\"].reenable\n        end\n        failed = ActualDbSchema.failed.map { |m| File.basename(m.filename) }\n        assert_equal(%w[20130906111513_irreversible_primary.rb 20130906111513_irreversible_secondary.rb], failed)\n        assert_equal(\n          %w[20130906111513_irreversible_primary.rb 20130906111513_irreversible_secondary.rb],\n          utils.migrated_files(TestingState.db_config)\n        )\n      end\n    end\n  end\n\n  describe \"db:phantom_migrations\" do\n    it \"shows the list of phantom migrations\" do\n      ActualDbSchema::Git.stub(:current_branch, \"fix-bug\") do\n        utils.prepare_phantom_migrations(TestingState.db_config)\n        Rake::Task[\"db:phantom_migrations\"].invoke\n        Rake::Task[\"db:phantom_migrations\"].reenable\n        assert_match(/ Status   Migration ID    Branch   Migration File/, TestingState.output)\n        assert_match(/---------------------------------------------------/, TestingState.output)\n        assert_match(\n          %r{   up     20130906111511  fix-bug  tmp/migrated/20130906111511_first_primary.rb},\n          TestingState.output\n        )\n        assert_match(\n          %r{   up     20130906111512  fix-bug  tmp/migrated/20130906111512_second_primary.rb},\n          TestingState.output\n        )\n        assert_match(\n          %r{   up     20130906111514  fix-bug  tmp/migrated_migrate_secondary/20130906111514_first_secondary.rb},\n          TestingState.output\n        )\n        assert_match(\n          %r{   up     20130906111515  fix-bug  tmp/migrated_migrate_secondary/20130906111515_second_secondary.rb},\n          TestingState.output\n        )\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "test/support/test_utils.rb",
    "content": "# frozen_string_literal: true\n\nclass TestUtils\n  attr_accessor :migrations_paths, :migrated_paths, :migration_timestamps, :connection_prefix\n\n  MIGRATED_PATHS = {\n    primary: \"tmp/migrated\",\n    secondary: \"tmp/migrated_migrate_secondary\"\n  }.freeze\n\n  MIGRATION_PATHS = {\n    primary: \"db/migrate\",\n    secondary: \"db/migrate_secondary\"\n  }.freeze\n\n  def initialize(migrations_path: \"db/migrate\", migrated_path: \"tmp/migrated\")\n    @migrations_paths = Array.wrap(migrations_path)\n    @migrated_paths = Array.wrap(migrated_path)\n    @migration_timestamps = %w[\n      20130906111511\n      20130906111512\n      20130906111514\n      20130906111515\n    ]\n  end\n\n  def app_file(path)\n    Rails.application.config.root.join(path)\n  end\n\n  def remove_app_dir(name)\n    FileUtils.rm_rf(app_file(name))\n  end\n\n  def run_migrations\n    schemas = ActualDbSchema.config[:multi_tenant_schemas]&.call\n    if schemas\n      schemas.each { |schema| ActualDbSchema::MultiTenant.with_schema(schema) { run_migration_tasks } }\n    else\n      run_migration_tasks\n    end\n  end\n\n  def applied_migrations(db_config = nil)\n    if db_config\n      db_config.each_with_object([]) do |(_, config), acc|\n        ActiveRecord::Base.establish_connection(**config)\n        acc.concat(applied_migrations_call)\n      end\n    else\n      applied_migrations_call\n    end\n  end\n\n  def simulate_input(input)\n    $stdin = StringIO.new(\"#{([input] * 999).join(\"\\n\")}\\n\")\n    yield\n  end\n\n  def delete_migrations_files(prefix_name = nil)\n    path = MIGRATION_PATHS.fetch(prefix_name&.to_sym, migrations_paths.first)\n    delete_migrations_files_for(path)\n  end\n\n  def delete_migrations_files_for(path)\n    Dir.glob(app_file(\"#{path}/**/*.rb\")).each do |file|\n      remove_app_dir(file)\n    end\n  end\n\n  def define_migration_file(filename, content, prefix: nil)\n    path =\n      case prefix\n      when \"primary\"\n        \"db/migrate\"\n      when \"secondary\"\n        \"db/migrate_secondary\"\n      when nil\n        migrations_paths.first\n      else\n        raise \"Unknown prefix: #{prefix}\"\n      end\n    File.write(app_file(\"#{path}/#{filename}\"), content, mode: \"w\")\n  end\n\n  def define_migrations(prefix_name = nil)\n    prefix = \"_#{prefix_name}\" if prefix_name\n    raise \"No migration timestamps left\" if @migration_timestamps.size < 2\n\n    {\n      first: \"#{@migration_timestamps.shift}_first#{prefix}.rb\",\n      second: \"#{@migration_timestamps.shift}_second#{prefix}.rb\"\n    }.each do |key, file_name|\n      define_migration_file(file_name, <<~RUBY, prefix: prefix_name)\n        class #{key.to_s.camelize}#{prefix_name.to_s.camelize} < ActiveRecord::Migration[6.0]\n          def up\n            TestingState.up << :#{key}#{prefix}\n          end\n\n          def down\n            TestingState.down << :#{key}#{prefix}\n          end\n        end\n      RUBY\n    end\n  end\n\n  def reset_database_yml(db_config)\n    database_yml_path = Rails.root.join(\"config\", \"database.yml\")\n    cleanup_config_files(db_config)\n    File.open(database_yml_path, \"w\") do |file|\n      file.write({\n        \"test\" => db_config\n      }.to_yaml)\n    end\n  end\n\n  def cleanup_config_files(db_config)\n    is_multi_db = db_config.is_a?(Hash) && db_config.key?(\"primary\")\n    configs = is_multi_db ? db_config.values : [db_config]\n    configs.each do |config|\n      database_path = Rails.root.join(config[\"database\"])\n      File.delete(database_path) if File.exist?(database_path)\n    end\n  end\n\n  def prepare_phantom_migrations(db_config = nil)\n    run_migrations\n    if db_config\n      db_config.each_key do |name|\n        delete_migrations_files(name) # simulate switching branches\n      end\n    else\n      delete_migrations_files\n    end\n  end\n\n  def cleanup(db_config = nil)\n    reset_acronyms\n    if db_config\n      db_config.each do |name, c|\n        ActiveRecord::Base.establish_connection(**c)\n        cleanup_call(name)\n      end\n    else\n      cleanup_call\n    end\n    TestingState.reset\n  end\n\n  def clear_db_storage_table(db_config = nil)\n    if db_config\n      db_config.each do |(_, config)|\n        ActiveRecord::Base.establish_connection(**config)\n        drop_db_storage_table\n      end\n    else\n      drop_db_storage_table\n    end\n  end\n\n  def drop_db_storage_table\n    return unless ActiveRecord::Base.connected?\n\n    conn = ActiveRecord::Base.connection\n    conn.drop_table(\"actual_db_schema_migrations\") if conn.table_exists?(\"actual_db_schema_migrations\")\n  end\n\n  def migrated_files(db_config = nil)\n    if db_config\n      db_config.each_with_object([]) do |(prefix_name, config), acc|\n        ActiveRecord::Base.establish_connection(**config)\n        acc.concat(migrated_files_call(prefix_name))\n      end\n    else\n      migrated_files_call\n    end\n  end\n\n  def branch_for(version)\n    metadata.fetch(version.to_s, {})[:branch]\n  end\n\n  def define_acronym(acronym)\n    ActiveSupport::Inflector.inflections(:en) do |inflect|\n      inflect.acronym acronym\n    end\n  end\n\n  def reset_acronyms\n    inflections = ActiveSupport::Inflector.inflections(:en)\n    return unless inflections.respond_to?(:acronyms)\n\n    inflections.acronyms.clear\n    inflections.send(:define_acronym_regex_patterns)\n  rescue NoMethodError\n    nil\n  end\n\n  def primary_database\n    TestingState.db_config[\"primary\"][\"database\"]\n  end\n\n  def secondary_database\n    TestingState.db_config[\"secondary\"][\"database\"]\n  end\n\n  private\n\n  def run_migration_tasks\n    if ActualDbSchema.config[:multi_tenant_schemas].present?\n      ActiveRecord::MigrationContext.new(Rails.root.join(\"db/migrate\"), schema_migration_class).migrate\n    end\n\n    Rake::Task[\"db:migrate\"].invoke\n    Rake::Task[\"db:migrate\"].reenable\n    Rake::Task[\"db:rollback_branches\"].reenable\n  end\n\n  def cleanup_call(prefix_name = nil)\n    delete_migrations_files(prefix_name)\n    create_schema_migration_table\n    clear_schema_call\n    remove_app_dir(MIGRATED_PATHS.fetch(prefix_name&.to_sym, migrated_paths.first))\n    define_migrations(prefix_name)\n    Rake::Task.clear\n    Rails.application.load_tasks\n  end\n\n  def create_schema_migration_table\n    schema_migration_class.create_table\n  end\n\n  def schema_migration_class\n    if ActiveRecord::SchemaMigration.respond_to?(:create_table)\n      ActiveRecord::SchemaMigration\n    else\n      ar_version = Gem::Version.new(ActiveRecord::VERSION::STRING)\n      if ar_version >= Gem::Version.new(\"7.2.0\") || (ar_version >= Gem::Version.new(\"7.1.0\") && ar_version.prerelease?)\n        ActiveRecord::SchemaMigration.new(ActiveRecord::Base.connection_pool)\n      else\n        ActiveRecord::SchemaMigration.new(ActiveRecord::Base.connection)\n      end\n    end\n  end\n\n  def migrated_files_call(prefix_name = nil)\n    migrated_path = ActualDbSchema.config[:migrated_folder].presence || migrated_paths.first\n    path = MIGRATED_PATHS.fetch(prefix_name&.to_sym, migrated_path.to_s)\n    Dir.glob(app_file(\"#{path}/*.rb\")).map { |f| File.basename(f) }.sort\n  end\n\n  def clear_schema_call\n    run_sql(\"delete from schema_migrations\")\n  end\n\n  def applied_migrations_call\n    run_sql(\"select version from schema_migrations order by version\").map do |row|\n      row.is_a?(Hash) ? row[\"version\"] : row[0]\n    end\n  end\n\n  def run_sql(sql)\n    ActiveRecord::Base.connection.execute(sql)\n  end\n\n  def metadata\n    ActualDbSchema::Store.instance.read\n  end\nend\n"
  },
  {
    "path": "test/test_actual_db_schema.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\nclass TestActualDbSchema < Minitest::Test\n  def test_that_it_has_a_version_number\n    refute_nil ::ActualDbSchema::VERSION\n  end\nend\n"
  },
  {
    "path": "test/test_actual_db_schema_db_storage_test.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\nclass TestActualDbSchemaDbStorage < Minitest::Test\n  def setup\n    ActualDbSchema.config[:migrations_storage] = :db\n  end\n\n  def teardown\n    ActualDbSchema.config[:migrations_storage] = :file\n  end\n\n  def test_that_it_has_a_version_number\n    refute_nil ::ActualDbSchema::VERSION\n  end\nend\n"
  },
  {
    "path": "test/test_database_filtering.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\ndescribe \"database filtering\" do\n  let(:utils) do\n    TestUtils.new(\n      migrations_path: [\"db/migrate\", \"db/migrate_secondary\"],\n      migrated_path: [\"tmp/migrated\", \"tmp/migrated_migrate_secondary\"]\n    )\n  end\n\n  # Helper to extract config name that works with Rails 6.0 (spec_name) and Rails 6.1+ (name)\n  def config_name(db_config)\n    if db_config.respond_to?(:name)\n      db_config.name.to_sym\n    elsif db_config.respond_to?(:spec_name)\n      db_config.spec_name.to_sym\n    else\n      :primary\n    end\n  end\n\n  before do\n    # Reset to default config\n    ActualDbSchema.config.excluded_databases = []\n  end\n\n  after do\n    # Clean up configuration after each test\n    ActualDbSchema.config.excluded_databases = []\n  end\n\n  describe \"with excluded_databases configuration\" do\n    it \"excludes databases from the excluded_databases list\" do\n      db_config = TestingState.db_config.dup\n      utils.reset_database_yml(db_config)\n      ActiveRecord::Base.configurations = { \"test\" => db_config }\n      ActiveRecord::Tasks::DatabaseTasks.database_configuration = { \"test\" => db_config }\n\n      # Configure to exclude secondary database\n      ActualDbSchema.config.excluded_databases = [:secondary]\n\n      # Get the migration context instance\n      context = ActualDbSchema::MigrationContext.instance\n\n      # Verify only primary database is included\n      configs = context.send(:configs)\n      config_names = configs.map { |c| config_name(c) }\n\n      assert_includes config_names, :primary\n      refute_includes config_names, :secondary\n    end\n\n    it \"allows excluding multiple databases\" do\n      db_config = {\n        \"primary\" => TestingState.db_config[\"primary\"],\n        \"secondary\" => TestingState.db_config[\"secondary\"],\n        \"queue\" => {\n          \"adapter\" => \"sqlite3\",\n          \"database\" => \"tmp/queue.sqlite3\",\n          \"migrations_paths\" => Rails.root.join(\"db\", \"migrate_queue\").to_s\n        }\n      }\n\n      utils.reset_database_yml(db_config)\n      ActiveRecord::Base.configurations = { \"test\" => db_config }\n      ActiveRecord::Tasks::DatabaseTasks.database_configuration = { \"test\" => db_config }\n\n      # Configure to exclude secondary and queue databases\n      ActualDbSchema.config.excluded_databases = %i[secondary queue]\n\n      # Get the migration context instance\n      context = ActualDbSchema::MigrationContext.instance\n\n      # Verify only primary database is included\n      configs = context.send(:configs)\n      config_names = configs.map { |c| config_name(c) }\n\n      assert_includes config_names, :primary\n      refute_includes config_names, :secondary\n      refute_includes config_names, :queue\n    end\n\n    it \"processes all databases when excluded_databases is empty\" do\n      db_config = TestingState.db_config.dup\n      utils.reset_database_yml(db_config)\n      ActiveRecord::Base.configurations = { \"test\" => db_config }\n      ActiveRecord::Tasks::DatabaseTasks.database_configuration = { \"test\" => db_config }\n\n      ActualDbSchema.config.excluded_databases = []\n\n      context = ActualDbSchema::MigrationContext.instance\n      configs = context.send(:configs)\n      config_names = configs.map { |c| config_name(c) }\n\n      assert_includes config_names, :primary\n      assert_includes config_names, :secondary\n    end\n  end\n\n  describe \"environment variable ACTUAL_DB_SCHEMA_EXCLUDED_DATABASES\" do\n    it \"parses comma-separated database names from environment variable\" do\n      ENV[\"ACTUAL_DB_SCHEMA_EXCLUDED_DATABASES\"] = \"queue,cable\"\n\n      # Create a new configuration to pick up the env var\n      config = ActualDbSchema::Configuration.new\n\n      assert_equal %i[queue cable], config.excluded_databases\n    ensure\n      ENV.delete(\"ACTUAL_DB_SCHEMA_EXCLUDED_DATABASES\")\n    end\n\n    it \"handles whitespace in environment variable\" do\n      ENV[\"ACTUAL_DB_SCHEMA_EXCLUDED_DATABASES\"] = \"queue, cable, cache\"\n\n      config = ActualDbSchema::Configuration.new\n\n      assert_equal %i[queue cable cache], config.excluded_databases\n    ensure\n      ENV.delete(\"ACTUAL_DB_SCHEMA_EXCLUDED_DATABASES\")\n    end\n\n    it \"returns empty array when environment variable is not set\" do\n      ENV.delete(\"ACTUAL_DB_SCHEMA_EXCLUDED_DATABASES\")\n\n      config = ActualDbSchema::Configuration.new\n\n      assert_equal [], config.excluded_databases\n    end\n\n    it \"handles empty string in environment variable\" do\n      ENV[\"ACTUAL_DB_SCHEMA_EXCLUDED_DATABASES\"] = \"\"\n\n      config = ActualDbSchema::Configuration.new\n\n      assert_equal [], config.excluded_databases\n    ensure\n      ENV.delete(\"ACTUAL_DB_SCHEMA_EXCLUDED_DATABASES\")\n    end\n\n    it \"filters out empty values from comma-separated list\" do\n      ENV[\"ACTUAL_DB_SCHEMA_EXCLUDED_DATABASES\"] = \"queue,,cable,  ,cache\"\n\n      config = ActualDbSchema::Configuration.new\n\n      assert_equal %i[queue cable cache], config.excluded_databases\n    ensure\n      ENV.delete(\"ACTUAL_DB_SCHEMA_EXCLUDED_DATABASES\")\n    end\n  end\nend\n"
  },
  {
    "path": "test/test_helper.rb",
    "content": "# frozen_string_literal: true\n\n$LOAD_PATH.unshift File.expand_path(\"../lib\", __dir__)\n\n# Clear DATABASE_URL to prevent it from overriding the test database configuration\nENV.delete(\"DATABASE_URL\")\n\nrequire \"logger\"\nrequire \"rails/all\"\nrequire \"actual_db_schema\"\nrequire \"minitest/autorun\"\nrequire \"debug\"\nrequire \"rake\"\nrequire \"fileutils\"\nrequire \"support/test_utils\"\n\nRails.env = \"test\"\n\nclass FakeApplication < Rails::Application\n  def initialize\n    super\n    config.root = File.join(__dir__, \"dummy_app\")\n  end\nend\n\nRails.application = FakeApplication.new\n\nclass TestingState\n  class << self\n    attr_accessor :up, :down, :output\n  end\n\n  def self.reset\n    self.up = []\n    self.down = []\n    ActualDbSchema.failed = []\n    self.output = +\"\"\n  end\n\n  def self.db_config\n    adapter = ENV.fetch(\"DB_ADAPTER\", \"sqlite3\")\n\n    case adapter\n    when \"sqlite3\"\n      sqlite3_config\n    when \"postgresql\"\n      postgresql_config\n    when \"mysql2\"\n      mysql2_config\n    else\n      raise \"Unsupported adapter: #{adapter}\"\n    end\n  end\n\n  def self.sqlite3_config\n    {\n      \"primary\" => {\n        \"adapter\" => \"sqlite3\",\n        \"database\" => \"tmp/primary.sqlite3\",\n        \"migrations_paths\" => Rails.root.join(\"db\", \"migrate\").to_s\n      },\n      \"secondary\" => {\n        \"adapter\" => \"sqlite3\",\n        \"database\" => \"tmp/secondary.sqlite3\",\n        \"migrations_paths\" => Rails.root.join(\"db\", \"migrate_secondary\").to_s\n      }\n    }\n  end\n\n  def self.postgresql_config\n    {\n      \"primary\" => {\n        \"adapter\" => \"postgresql\",\n        \"database\" => \"actual_db_schema_test\",\n        \"username\" => \"postgres\",\n        \"password\" => \"password\",\n        \"host\" => \"localhost\",\n        \"port\" => 5432,\n        \"migrations_paths\" => Rails.root.join(\"db\", \"migrate\").to_s\n      },\n      \"secondary\" => {\n        \"adapter\" => \"postgresql\",\n        \"database\" => \"actual_db_schema_test_secondary\",\n        \"username\" => \"postgres\",\n        \"password\" => \"password\",\n        \"host\" => \"localhost\",\n        \"port\" => 5432,\n        \"migrations_paths\" => Rails.root.join(\"db\", \"migrate_secondary\").to_s\n      }\n    }\n  end\n\n  def self.mysql2_config\n    {\n      \"primary\" => {\n        \"adapter\" => \"mysql2\",\n        \"database\" => \"actual_db_schema_test\",\n        \"username\" => \"root\",\n        \"password\" => \"password\",\n        \"host\" => \"127.0.0.1\",\n        \"port\" => \"3306\",\n        \"migrations_paths\" => Rails.root.join(\"db\", \"migrate\").to_s\n      },\n      \"secondary\" => {\n        \"adapter\" => \"mysql2\",\n        \"database\" => \"actual_db_schema_test_secondary\",\n        \"username\" => \"root\",\n        \"password\" => \"password\",\n        \"host\" => \"127.0.0.1\",\n        \"port\" => \"3306\",\n        \"migrations_paths\" => Rails.root.join(\"db\", \"migrate_secondary\").to_s\n      }\n    }\n  end\n\n  reset\nend\n\nActualDbSchema.config[:enabled] = true\n\nmodule Minitest\n  class Test\n    def before_setup\n      super\n      if defined?(ActualDbSchema)\n        ActualDbSchema::Store.instance.reset_adapter\n        ActualDbSchema.failed = []\n      end\n      cleanup_migrated_cache if defined?(Rails) && Rails.respond_to?(:root)\n      clear_db_storage_tables if defined?(TestingState)\n      ActualDbSchema.config[:migrations_storage] = :file if defined?(ActualDbSchema)\n      return unless defined?(ActualDbSchema::Migration)\n\n      ActualDbSchema::Migration.instance.instance_variable_set(:@metadata, {})\n    end\n\n    private\n\n    def cleanup_migrated_cache\n      Dir.glob(Rails.root.join(\"tmp\", \"migrated*\")).each { |path| FileUtils.rm_rf(path) }\n      FileUtils.rm_rf(Rails.root.join(\"custom\", \"migrated\"))\n    end\n\n    def clear_db_storage_tables\n      db_storage_configs.each do |config|\n        ActiveRecord::Base.establish_connection(**config)\n        drop_db_storage_table(ActiveRecord::Base.connection)\n      rescue StandardError\n        next\n      end\n    end\n\n    def db_storage_configs\n      db_config = TestingState.db_config\n      return db_config.values if db_config.is_a?(Hash) && db_config.key?(\"primary\")\n\n      [db_config]\n    end\n\n    def drop_db_storage_table(conn)\n      table_name = \"actual_db_schema_migrations\"\n      if conn.adapter_name =~ /postgresql|mysql/i\n        drop_db_storage_table_in_schemas(conn, table_name)\n      elsif conn.table_exists?(table_name)\n        conn.drop_table(table_name)\n      end\n    end\n\n    def drop_db_storage_table_in_schemas(conn, table_name)\n      schemas = conn.select_values(<<~SQL.squish)\n        SELECT table_schema\n        FROM information_schema.tables\n        WHERE table_name = #{conn.quote(table_name)}\n      SQL\n      schemas.each do |schema|\n        conn.execute(\"DROP TABLE IF EXISTS #{conn.quote_table_name(schema)}.#{conn.quote_table_name(table_name)}\")\n      end\n    end\n  end\nend\n\nmodule Kernel\n  alias original_puts puts\n\n  def puts(*args)\n    TestingState.output << args.join(\"\\n\")\n    original_puts(*args)\n  end\nend\n"
  },
  {
    "path": "test/test_migration_context.rb",
    "content": "# frozen_string_literal: true\n\nrequire \"test_helper\"\n\ndescribe \"ActualDbSchema::MigrationContext#each\" do\n  let(:utils) do\n    TestUtils.new(\n      migrations_path: [\"db/migrate\", \"db/migrate_secondary\"],\n      migrated_path: [\"tmp/migrated\", \"tmp/migrated_migrate_secondary\"]\n    )\n  end\n\n  before do\n    utils.reset_database_yml(TestingState.db_config)\n    ActiveRecord::Base.configurations = { \"test\" => TestingState.db_config }\n    ActiveRecord::Tasks::DatabaseTasks.database_configuration = { \"test\" => TestingState.db_config }\n    utils.cleanup(TestingState.db_config)\n    # Establish connection to primary as the \"original\" connection before iterating\n    ActiveRecord::Base.establish_connection(**TestingState.db_config[\"primary\"])\n  end\n\n  it \"restores the original connection after iterating over multiple databases\" do\n    primary_db = File.basename(TestingState.db_config[\"primary\"][\"database\"])\n\n    # Iterating switches the connection to each database in turn (primary, then secondary)\n    ActualDbSchema::MigrationContext.instance.each { |_context| }\n\n    # After iteration, the connection must be restored to the original (primary) database.\n    # Without restoration, the connection is left on the last database (secondary), which\n    # means any subsequent ActiveRecord queries silently hit the wrong database.\n    current_db = File.basename(current_database)\n    assert_equal primary_db, current_db,\n                 \"MigrationContext#each must restore the original connection after iteration, \" \\\n                 \"but was left on '#{current_db}' instead of '#{primary_db}'\"\n  end\n\n  private\n\n  def current_database\n    if ActiveRecord::Base.respond_to?(:connection_db_config)\n      ActiveRecord::Base.connection_db_config.database\n    else\n      ActiveRecord::Base.connection_config[:database]\n    end\n  end\nend\n"
  }
]