├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── ---bug-report.md │ └── ---feature-request.md └── workflows │ ├── code-style.yml │ ├── stale.yml │ ├── static-analysis.yml │ └── test.yml ├── .gitignore ├── .php-cs-fixer.dist.php ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── composer.json ├── config └── elastic.migrations.php ├── database └── migrations │ └── 2019_15_12_112000_create_elastic_migrations_table.php ├── ide.json ├── phpstan-baseline.neon ├── phpstan.neon.dist ├── phpunit.xml.dist ├── src ├── Adapters │ └── IndexManagerAdapter.php ├── Console │ ├── FreshCommand.php │ ├── MakeCommand.php │ ├── MigrateCommand.php │ ├── RefreshCommand.php │ ├── ResetCommand.php │ ├── RollbackCommand.php │ ├── StatusCommand.php │ └── stubs │ │ └── migration.blank.stub ├── Facades │ └── Index.php ├── Factories │ └── MigrationFactory.php ├── Filesystem │ ├── MigrationFile.php │ └── MigrationStorage.php ├── IndexManagerInterface.php ├── MigrationInterface.php ├── Migrator.php ├── ReadinessInterface.php ├── Repositories │ └── MigrationRepository.php ├── ServiceProvider.php └── helpers.php └── tests ├── Integration ├── Adapters │ └── IndexManagerAdapterTest.php ├── Console │ ├── FreshCommandTest.php │ ├── MakeCommandTest.php │ ├── MigrateCommandTest.php │ ├── RefreshCommandTest.php │ ├── ResetCommandTest.php │ ├── RollbackCommandTest.php │ └── StatusCommandTest.php ├── Facades │ └── IndexTest.php ├── Factories │ └── MigrationFactoryTest.php ├── Filesystem │ └── MigrationStorageTest.php ├── MigratorTest.php ├── Repositories │ └── MigrationRepositoryTest.php └── TestCase.php ├── Unit └── Filesystem │ └── MigrationFileTest.php └── migrations ├── 2018_12_01_081000_create_test_index.php ├── 2019_08_10_142230_update_test_index_mapping.php ├── archive └── 2017_11_11_100000_create_test_alias.php └── non_migration_file /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: ivanbabenko 2 | custom: ['https://paypal.me/babenkoi'] 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41E Bug report" 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | 15 | 16 | | Software | Version 17 | | ------------- | --------------- 18 | | PHP | x.y.z 19 | | Elasticsearch | x.y.z 20 | | Laravel | x.y.z 21 | 22 | **Describe the bug** 23 | 24 | 25 | **To Reproduce** 26 | 27 | 28 | **Current behavior** 29 | 30 | 31 | **Expected behavior** 32 | 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F4A1 Feature request" 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/workflows/code-style.yml: -------------------------------------------------------------------------------- 1 | name: Code style 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | style-check: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout code 10 | uses: actions/checkout@v2 11 | 12 | - name: Install php and composer 13 | uses: shivammathur/setup-php@v2 14 | with: 15 | php-version: 8.2 16 | coverage: none 17 | tools: composer:v2 18 | 19 | - name: Get composer cache directory 20 | id: composer-cache 21 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 22 | 23 | - name: Restore composer cache 24 | uses: actions/cache@v4 25 | with: 26 | path: ${{ steps.composer-cache.outputs.dir }} 27 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} 28 | restore-keys: ${{ runner.os }}-composer- 29 | 30 | - name: Install dependencies 31 | run: composer install --no-interaction 32 | 33 | - name: Check code style 34 | run: make style-check 35 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Close stale issues and pull requests 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | 7 | jobs: 8 | stale: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/stale@v3 12 | with: 13 | repo-token: ${{ secrets.GITHUB_TOKEN }} 14 | stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 7 days' 15 | stale-pr-message: 'This pull request is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 7 days' 16 | stale-issue-label: 'stale' 17 | stale-pr-label: 'stale' 18 | days-before-stale: 30 19 | days-before-close: 7 20 | -------------------------------------------------------------------------------- /.github/workflows/static-analysis.yml: -------------------------------------------------------------------------------- 1 | name: Static analysis 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | static-analysis: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout code 10 | uses: actions/checkout@v2 11 | 12 | - name: Install php and composer 13 | uses: shivammathur/setup-php@v2 14 | with: 15 | php-version: 8.2 16 | coverage: none 17 | tools: composer:v2 18 | 19 | - name: Get composer cache directory 20 | id: composer-cache 21 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 22 | 23 | - name: Restore composer cache 24 | uses: actions/cache@v4 25 | with: 26 | path: ${{ steps.composer-cache.outputs.dir }} 27 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} 28 | restore-keys: ${{ runner.os }}-composer- 29 | 30 | - name: Install dependencies 31 | run: composer install --no-interaction 32 | 33 | - name: Analyse code 34 | run: make static-analysis 35 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | php: [8.2] 11 | include: 12 | - php: 8.2 13 | testbench: 9.0 14 | phpunit: 11.0 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | 19 | - name: Install php and composer 20 | uses: shivammathur/setup-php@v2 21 | with: 22 | php-version: ${{ matrix.php }} 23 | coverage: none 24 | tools: composer:v2 25 | 26 | - name: Get composer cache directory 27 | id: composer-cache 28 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 29 | 30 | - name: Restore composer cache 31 | uses: actions/cache@v4 32 | with: 33 | path: ${{ steps.composer-cache.outputs.dir }} 34 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} 35 | restore-keys: ${{ runner.os }}-composer- 36 | 37 | - name: Install dependencies 38 | run: composer require --no-interaction --dev orchestra/testbench:^${{ matrix.testbench }} phpunit/phpunit:^${{ matrix.phpunit }} 39 | 40 | - name: Run tests 41 | run: make test 42 | 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /bin 3 | /vendor 4 | /composer.lock 5 | /phpunit.xml 6 | /.phpunit.cache 7 | /.phpunit.result.cache 8 | /.php_cs 9 | /.php_cs.cache 10 | /.php-cs-fixer.cache 11 | /phpstan.neon 12 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | in(__DIR__ . '/src') 8 | ->in(__DIR__ . '/tests') 9 | ->name('*.php'); 10 | 11 | return (new Config()) 12 | ->setFinder($finder) 13 | ->setRules([ 14 | '@PSR2' => true, 15 | 'array_syntax' => ['syntax' => 'short'], 16 | 'compact_nullable_type_declaration' => true, 17 | 'concat_space' => ['spacing' => 'one'], 18 | 'declare_strict_types' => true, 19 | 'dir_constant' => true, 20 | 'self_static_accessor' => false, 21 | 'fully_qualified_strict_types' => true, 22 | 'function_to_constant' => true, 23 | 'type_declaration_spaces' => true, 24 | 'header_comment' => false, 25 | 'list_syntax' => ['syntax' => 'short'], 26 | 'lowercase_cast' => true, 27 | 'magic_method_casing' => true, 28 | 'modernize_types_casting' => true, 29 | 'multiline_comment_opening_closing' => true, 30 | 'native_constant_invocation' => true, 31 | 'no_alias_functions' => true, 32 | 'no_alternative_syntax' => true, 33 | 'no_blank_lines_after_phpdoc' => true, 34 | 'no_empty_comment' => true, 35 | 'no_empty_phpdoc' => true, 36 | 'no_extra_blank_lines' => true, 37 | 'no_leading_import_slash' => true, 38 | 'no_leading_namespace_whitespace' => true, 39 | 'no_spaces_around_offset' => true, 40 | 'no_superfluous_phpdoc_tags' => ['allow_mixed' => true], 41 | 'no_trailing_comma_in_singleline' => true, 42 | 'no_unneeded_control_parentheses' => true, 43 | 'no_unset_cast' => true, 44 | 'no_unused_imports' => true, 45 | 'no_useless_else' => true, 46 | 'no_useless_return' => true, 47 | 'no_whitespace_in_blank_line' => true, 48 | 'normalize_index_brace' => true, 49 | 'ordered_imports' => true, 50 | 'php_unit_construct' => true, 51 | 'php_unit_dedicate_assert' => ['target' => 'newest'], 52 | 'php_unit_dedicate_assert_internal_type' => ['target' => 'newest'], 53 | 'php_unit_expectation' => ['target' => 'newest'], 54 | 'php_unit_mock' => ['target' => 'newest'], 55 | 'php_unit_mock_short_will_return' => true, 56 | 'php_unit_no_expectation_annotation' => ['target' => 'newest'], 57 | 'php_unit_test_annotation' => ['style' => 'prefix'], 58 | 'php_unit_test_case_static_method_calls' => ['call_type' => 'this'], 59 | 'phpdoc_align' => ['align' => 'vertical'], 60 | 'phpdoc_line_span' => ['method' => 'multi', 'property' => 'multi'], 61 | 'phpdoc_no_package' => true, 62 | 'phpdoc_no_useless_inheritdoc' => true, 63 | 'phpdoc_scalar' => true, 64 | 'phpdoc_separation' => true, 65 | 'phpdoc_single_line_var_spacing' => true, 66 | 'phpdoc_trim' => true, 67 | 'phpdoc_trim_consecutive_blank_line_separation' => true, 68 | 'phpdoc_types' => true, 69 | 'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'], 70 | 'phpdoc_var_without_name' => true, 71 | 'return_assignment' => true, 72 | 'short_scalar_cast' => true, 73 | 'single_trait_insert_per_statement' => true, 74 | 'standardize_not_equals' => true, 75 | 'static_lambda' => true, 76 | 'ternary_to_null_coalescing' => true, 77 | 'trim_array_spaces' => true, 78 | 'array_indentation' => true, 79 | 'trailing_comma_in_multiline' => true, 80 | 'visibility_required' => true, 81 | 'yoda_style' => false, 82 | 'use_arrow_functions' => true, 83 | 'phpdoc_to_property_type' => false 84 | ]); 85 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at babenko.i.a@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Workflow 2 | 3 | * Fork the project and clone it locally 4 | * Create a new branch for every new feature or a bug fix 5 | * Do the necessary code changes 6 | * Cover the new or fixed code with tests 7 | * Write a comprehensive commit message in a format `Add the xxx feature` or `Fix the xxx bug` 8 | * Push to the forked repository 9 | * Create a Pull Request to the master branch of the original repository 10 | * Make a new commit with a fix if one or more checks are failing (code analysis, tests, etc.) 11 | 12 | ## Pull Request Requirements 13 | 14 | * Follow [PSR-2 coding style standard](https://www.php-fig.org/psr/psr-2/) 15 | * Write tests 16 | * Document every new feature or an interface change in the README file 17 | * Make one Pull Request per feature / bug fix 18 | 19 | ## Running the Test Suite 20 | 21 | To run tests locally you need PHP (7.2 or higher), [Composer](https://getcomposer.org/download/) and [SQLite 3](https://www.sqlite.org/download.html). 22 | 23 | Install the project dependencies: 24 | ``` 25 | composer install 26 | ``` 27 | 28 | Run the test suite: 29 | ``` 30 | make test 31 | ``` 32 | 33 | ## Code Analysis 34 | 35 | To ensure, that your code follows PSR-2 standards you can run: 36 | ``` 37 | make style-check 38 | ``` 39 | 40 | It is also recommended to perform static code analysis before opening a PR: 41 | ``` 42 | make static-analysis 43 | ``` 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ivan Babenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test coverage style-check static-analysis help 2 | 3 | .DEFAULT_GOAL := help 4 | 5 | test: ## Run tests 6 | @printf "\033[93m→ Running tests\033[0m\n" 7 | @bin/phpunit --testdox 8 | @printf "\n\033[92m✔︎ Tests are completed\033[0m\n" 9 | 10 | coverage: ## Run tests and generate the code coverage report 11 | @printf "\033[93m→ Running tests and generating the code coverage report\033[0m\n" 12 | @XDEBUG_MODE=coverage bin/phpunit --testdox --coverage-text 13 | @printf "\n\033[92m✔︎ Tests are completed and the report is generated\033[0m\n" 14 | 15 | style-check: ## Check the code style 16 | @printf "\033[93m→ Checking the code style\033[0m\n" 17 | @bin/php-cs-fixer fix --allow-risky=yes --dry-run --diff --show-progress=dots --verbose 18 | @printf "\n\033[92m✔︎ Code style is checked\033[0m\n" 19 | 20 | static-analysis: ## Do static code analysis 21 | @printf "\033[93m→ Analysing the code\033[0m\n" 22 | @php -d memory_limit=-1 bin/phpstan analyse 23 | @printf "\n\033[92m✔︎ Code static analysis is completed\033[0m\n" 24 | 25 | help: ## Show help 26 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elastic Migrations 2 | 3 | [![Latest Stable Version](https://poser.pugx.org/babenkoivan/elastic-migrations/v/stable)](https://packagist.org/packages/babenkoivan/elastic-migrations) 4 | [![Total Downloads](https://poser.pugx.org/babenkoivan/elastic-migrations/downloads)](https://packagist.org/packages/babenkoivan/elastic-migrations) 5 | [![License](https://poser.pugx.org/babenkoivan/elastic-migrations/license)](https://packagist.org/packages/babenkoivan/elastic-migrations) 6 | [![Tests](https://github.com/babenkoivan/elastic-migrations/workflows/Tests/badge.svg)](https://github.com/babenkoivan/elastic-migrations/actions?query=workflow%3ATests) 7 | [![Code style](https://github.com/babenkoivan/elastic-migrations/workflows/Code%20style/badge.svg)](https://github.com/babenkoivan/elastic-migrations/actions?query=workflow%3A%22Code+style%22) 8 | [![Static analysis](https://github.com/babenkoivan/elastic-migrations/workflows/Static%20analysis/badge.svg)](https://github.com/babenkoivan/elastic-migrations/actions?query=workflow%3A%22Static+analysis%22) 9 | [![Donate PayPal](https://img.shields.io/badge/donate-paypal-blue)](https://paypal.me/babenkoi) 10 | 11 |

12 | Support the project! 13 |

14 | 15 | --- 16 | 17 | Elastic Migrations for Laravel allow you to easily modify and share indices schema across the application's environments. 18 | 19 | ## Contents 20 | 21 | * [Compatibility](#compatibility) 22 | * [Installation](#installation) 23 | * [Configuration](#configuration) 24 | * [Writing Migrations](#writing-migrations) 25 | * [Running Migrations](#running-migrations) 26 | * [Reverting Migrations](#reverting-migrations) 27 | * [Starting Over](#starting-over) 28 | * [Migration Status](#migration-status) 29 | * [Zero Downtime Migration](#zero-downtime-migration) 30 | * [Troubleshooting](#migration-status) 31 | 32 | ## Compatibility 33 | 34 | The current version of Elastic Migrations has been tested with the following configuration: 35 | 36 | * PHP 8.2 37 | * Elasticsearch 8.x 38 | * Laravel 11.x 39 | 40 | If your project uses older Laravel (or PHP) version check [the previous major version](https://github.com/babenkoivan/elastic-migrations/tree/v3.4.1#compatibility) of the package. 41 | 42 | ## Installation 43 | 44 | The library can be installed via Composer: 45 | 46 | ```bash 47 | composer require babenkoivan/elastic-migrations 48 | ``` 49 | 50 | If you want to use Elastic Migrations with [Lumen framework](https://lumen.laravel.com/) check [this guide](https://github.com/babenkoivan/elastic-migrations/wiki/Lumen-Installation). 51 | 52 | ## Configuration 53 | 54 | Elastic Migrations uses [babenkoivan/elastic-client](https://github.com/babenkoivan/elastic-client) as a dependency. 55 | To change the client settings you need to publish the configuration file first: 56 | 57 | ```bash 58 | php artisan vendor:publish --provider="Elastic\Client\ServiceProvider" 59 | ``` 60 | 61 | In the newly created `config/elastic.client.php` file you can define the default connection name and describe multiple 62 | connections using configuration hashes. Please, refer to the [elastic-client documentation](https://github.com/babenkoivan/elastic-client) for more details. 63 | 64 | It is recommended to publish Elastic Migrations settings as well: 65 | 66 | ```bash 67 | php artisan vendor:publish --provider="Elastic\Migrations\ServiceProvider" 68 | ``` 69 | 70 | This will create the `config/elastic.migrations.php` file, which allows you to configure the following options: 71 | 72 | * `storage.default_path` - the default location of your migration files 73 | * `database.table` - the table name that holds executed migration names 74 | * `database.connection` - the database connection you wish to use 75 | * `prefixes.index` - the prefix of your indices 76 | * `prefixes.alias` - the prefix of your aliases 77 | 78 | If you store some migration files outside the default path and want them to be visible by the package, you may use 79 | `registerPaths` method to inform Elastic Migrations how to load them: 80 | 81 | ```php 82 | class MyAppServiceProvider extends Illuminate\Support\ServiceProvider 83 | { 84 | public function boot() 85 | { 86 | resolve(MigrationStorage::class)->registerPaths([ 87 | '/my_app/elastic/migrations1', 88 | '/my_app/elastic/migrations2', 89 | ]); 90 | } 91 | } 92 | ``` 93 | 94 | 95 | Finally, don't forget to run Laravel database migrations to create Elastic Migrations table: 96 | 97 | ```bash 98 | php artisan migrate 99 | ``` 100 | 101 | ## Writing Migrations 102 | 103 | You can effortlessly create a new migration file using an Artisan console command: 104 | 105 | ```bash 106 | // create a migration file with "create_my_index.php" name in the default directory 107 | php artisan elastic:make:migration create_my_index 108 | 109 | // create a migration file with "create_my_index.php" name in "/my_path" directory 110 | // note, that you need to specify the full path to the file in this case 111 | php artisan elastic:make:migration /my_path/create_my_index.php 112 | ``` 113 | 114 | Every migration has two methods: `up` and `down`. `up` is used to alternate the index schema and `down` is used to revert that action. 115 | 116 | You can use `Elastic\Migrations\Facades\Index` facade to perform basic operations over Elasticsearch indices: 117 | 118 | #### Create Index 119 | 120 | You can create an index with the default settings: 121 | 122 | ```php 123 | Index::create('my-index'); 124 | ``` 125 | 126 | You can use a modifier to configure mapping and settings: 127 | 128 | ```php 129 | Index::create('my-index', function (Mapping $mapping, Settings $settings) { 130 | // to add a new field to the mapping use method name as a field type (in Camel Case), 131 | // first argument as a field name and optional second argument for additional field parameters 132 | $mapping->text('title', ['boost' => 2]); 133 | $mapping->float('price'); 134 | 135 | // you can define a dynamic template as follows 136 | $mapping->dynamicTemplate('my_template_name', [ 137 | 'match_mapping_type' => 'long', 138 | 'mapping' => [ 139 | 'type' => 'integer', 140 | ], 141 | ]); 142 | 143 | // you can also change the index settings and the analysis configuration 144 | $settings->index([ 145 | 'number_of_replicas' => 2, 146 | 'refresh_interval' => -1 147 | ]); 148 | 149 | $settings->analysis([ 150 | 'analyzer' => [ 151 | 'title' => [ 152 | 'type' => 'custom', 153 | 'tokenizer' => 'whitespace' 154 | ] 155 | ] 156 | ]); 157 | }); 158 | ``` 159 | 160 | There is also the `createRaw` method in your disposal: 161 | 162 | ```php 163 | $mapping = [ 164 | 'properties' => [ 165 | 'title' => [ 166 | 'type' => 'text' 167 | ] 168 | ] 169 | ]; 170 | 171 | $settings = [ 172 | 'number_of_replicas' => 2 173 | ]; 174 | 175 | Index::createRaw('my-index', $mapping, $settings); 176 | ``` 177 | 178 | Finally, it is possible to create an index only if it doesn't exist: 179 | 180 | ```php 181 | // you can use a modifier as shown above 182 | Index::createIfNotExists('my-index', $modifier); 183 | // or you can use raw mapping and settings 184 | Index::createIfNotExistsRaw('my-index', $mapping, $settings); 185 | ``` 186 | 187 | #### Update Mapping 188 | 189 | You can use a modifier to adjust the mapping: 190 | 191 | ```php 192 | Index::putMapping('my-index', function (Mapping $mapping) { 193 | $mapping->text('title', ['boost' => 2]); 194 | $mapping->float('price'); 195 | }); 196 | ``` 197 | 198 | Alternatively, you can use the `putMappingRaw` method as follows: 199 | 200 | ```php 201 | Index::putMappingRaw('my-index', [ 202 | 'properties' => [ 203 | 'title' => [ 204 | 'type' => 'text', 205 | 'boost' => 2 206 | ], 207 | 'price' => [ 208 | 'price' => 'float' 209 | ] 210 | ] 211 | ]); 212 | ``` 213 | 214 | #### Update Settings 215 | 216 | You can use a modifier to change an index configuration: 217 | 218 | ```php 219 | Index::putSettings('my-index', function (Settings $settings) { 220 | $settings->index([ 221 | 'number_of_replicas' => 2, 222 | 'refresh_interval' => -1 223 | ]); 224 | }); 225 | ``` 226 | 227 | The same result can be achieved with the `putSettingsRaw` method: 228 | 229 | ```php 230 | Index::putSettingsRaw('my-index', [ 231 | 'index' => [ 232 | 'number_of_replicas' => 2, 233 | 'refresh_interval' => -1 234 | ] 235 | ]); 236 | ``` 237 | 238 | It is possible to update analysis settings only on closed indices. The `pushSettings` method closes the index, 239 | updates the configuration and opens the index again: 240 | 241 | ```php 242 | Index::pushSettings('my-index', function (Settings $settings) { 243 | $settings->analysis([ 244 | 'analyzer' => [ 245 | 'title' => [ 246 | 'type' => 'custom', 247 | 'tokenizer' => 'whitespace' 248 | ] 249 | ] 250 | ]); 251 | }); 252 | ``` 253 | 254 | The same can be done with the `pushSettingsRaw` method: 255 | 256 | ```php 257 | Index::pushSettingsRaw('my-index', [ 258 | 'analysis' => [ 259 | 'analyzer' => [ 260 | 'title' => [ 261 | 'type' => 'custom', 262 | 'tokenizer' => 'whitespace' 263 | ] 264 | ] 265 | ] 266 | ]); 267 | ``` 268 | 269 | #### Drop Index 270 | 271 | You can unconditionally delete the index: 272 | 273 | ```php 274 | Index::drop('my-index'); 275 | ``` 276 | 277 | or delete it only if it exists: 278 | 279 | ```php 280 | Index::dropIfExists('my-index'); 281 | ``` 282 | 283 | #### Create Alias 284 | 285 | You can create an alias with optional filter query: 286 | 287 | ```php 288 | Index::putAlias('my-index', 'my-alias', [ 289 | 'is_write_index' => true, 290 | 'filter' => [ 291 | 'term' => [ 292 | 'user_id' => 1, 293 | ], 294 | ], 295 | ]); 296 | ``` 297 | 298 | #### Delete Alias 299 | 300 | You can delete an alias by its name: 301 | 302 | ```php 303 | Index::deleteAlias('my-index', 'my-alias'); 304 | ``` 305 | 306 | #### Multiple Connections 307 | 308 | You can configure multiple connections to Elasticsearch in the [client's configuration file](https://github.com/babenkoivan/elastic-client/tree/master#configuration), 309 | and then use a different connection for every operation: 310 | 311 | ```php 312 | Index::connection('my-connection')->drop('my-index'); 313 | ``` 314 | 315 | #### More 316 | 317 | Finally, you are free to inject `Elastic\Elasticsearch\Client` in the migration constructor and execute any supported by client actions. 318 | 319 | ## Running Migrations 320 | 321 | You can either run all migrations: 322 | 323 | ```bash 324 | php artisan elastic:migrate 325 | ``` 326 | 327 | or run a specific one: 328 | 329 | ```bash 330 | // execute a migration located in one of the registered paths 331 | php artisan elastic:migrate 2018_12_01_081000_create_my_index 332 | 333 | // execute a migration located in "/my_path" directory 334 | // note, that you need to specify the full path to the file in this case 335 | php artisan elastic:migrate /my_path/2018_12_01_081000_create_my_index.php 336 | ``` 337 | 338 | Use the `--force` option if you want to execute migrations on production environment: 339 | 340 | ```bash 341 | php artisan elastic:migrate --force 342 | ``` 343 | 344 | ## Reverting Migrations 345 | 346 | You can either revert the last executed migrations: 347 | 348 | ```bash 349 | php artisan elastic:migrate:rollback 350 | ``` 351 | 352 | or rollback a specific one: 353 | 354 | ```bash 355 | // rollback a migration located in one of the registered paths 356 | php artisan elastic:migrate:rollback 2018_12_01_081000_create_my_index 357 | 358 | // rollback a migration located in "/my_path" directory 359 | // note, that you need to specify the full path to the file in this case 360 | php artisan elastic:migrate:rollback /my_path/2018_12_01_081000_create_my_index 361 | ``` 362 | 363 | Use the `elastic:migrate:reset` command if you want to revert all previously migrated files: 364 | 365 | ```bash 366 | php artisan elastic:migrate:reset 367 | ``` 368 | 369 | ## Starting Over 370 | 371 | Sometimes you just want to start over, rollback all the changes and apply them again: 372 | 373 | ```bash 374 | php artisan elastic:migrate:refresh 375 | ``` 376 | 377 | Alternatively you can also drop all existing indices and rerun the migrations: 378 | 379 | ```bash 380 | php artisan elastic:migrate:fresh 381 | ``` 382 | 383 | **Note** that this command uses wildcards to delete indices. This requires setting [action.destructive_requires_name](https://www.elastic.co/guide/en/elasticsearch/reference/current/index-management-settings.html#action-destructive-requires-name) to `false`. 384 | 385 | ## Migration Status 386 | 387 | You can always check which files have been already migrated and what can be reverted by the `elastic:migrate:rollback` command (the last batch): 388 | 389 | ```bash 390 | php artisan elastic:migrate:status 391 | ``` 392 | 393 | It is also possible to display only pending migrations: 394 | 395 | ```bash 396 | php artisan elastic:migrate:status --pending 397 | ``` 398 | 399 | ## Zero Downtime Migration 400 | 401 | Changing an index mapping with zero downtime is not a trivial process and might vary from one project to another. 402 | Elastic Migrations library doesn't include such feature out of the box, but you can implement it in your project by [following this guide](https://github.com/babenkoivan/elastic-migrations/wiki/Changing-Mapping-with-Zero-Downtime). 403 | 404 | ## Troubleshooting 405 | 406 | If you see one of the messages below, follow the instructions: 407 | 408 | * `Migration table is not yet created` - run the `php artisan migrate` command 409 | * `Migration directory is not yet created` - create a migration file using the `elastic:make:migration` command or 410 | create `migrations` directory manually 411 | 412 | In case one of the commands doesn't work as expected, try to publish configuration: 413 | 414 | ```bash 415 | php artisan vendor:publish --provider="Elastic\Migrations\ServiceProvider" 416 | ``` 417 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babenkoivan/elastic-migrations", 3 | "description": "Elasticsearch migrations for Laravel", 4 | "keywords": [ 5 | "laravel", 6 | "migrations", 7 | "elastic", 8 | "elasticsearch", 9 | "php" 10 | ], 11 | "type": "library", 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Ivan Babenko", 16 | "email": "babenko.i.a@gmail.com" 17 | } 18 | ], 19 | "funding": [ 20 | { 21 | "type": "ko-fi", 22 | "url": "https://ko-fi.com/ivanbabenko" 23 | }, 24 | { 25 | "type": "paypal", 26 | "url": "https://paypal.me/babenkoi" 27 | } 28 | ], 29 | "autoload": { 30 | "psr-4": { 31 | "Elastic\\Migrations\\": "src" 32 | }, 33 | "files": [ 34 | "src/helpers.php" 35 | ] 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "Elastic\\Migrations\\Tests\\": "tests" 40 | } 41 | }, 42 | "require": { 43 | "php": "^8.2", 44 | "babenkoivan/elastic-adapter": "^4.0" 45 | }, 46 | "require-dev": { 47 | "phpunit/phpunit": "^11.0", 48 | "orchestra/testbench": "^9.0", 49 | "friendsofphp/php-cs-fixer": "^3.14", 50 | "phpstan/phpstan": "^1.10", 51 | "dg/bypass-finals": "^1.7" 52 | }, 53 | "config": { 54 | "bin-dir": "bin", 55 | "allow-plugins": { 56 | "php-http/discovery": true 57 | } 58 | }, 59 | "extra": { 60 | "laravel": { 61 | "providers": [ 62 | "Elastic\\Migrations\\ServiceProvider" 63 | ] 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /config/elastic.migrations.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'default_path' => env('ELASTIC_MIGRATIONS_DEFAULT_PATH', base_path('elastic/migrations')) 6 | ], 7 | 'database' => [ 8 | 'table' => env('ELASTIC_MIGRATIONS_TABLE', 'elastic_migrations'), 9 | 'connection' => env('ELASTIC_MIGRATIONS_CONNECTION'), 10 | ], 11 | 'prefixes' => [ 12 | 'index' => env('ELASTIC_MIGRATIONS_INDEX_PREFIX', env('SCOUT_PREFIX', '')), 13 | 'alias' => env('ELASTIC_MIGRATIONS_ALIAS_PREFIX', env('SCOUT_PREFIX', '')), 14 | ], 15 | ]; 16 | -------------------------------------------------------------------------------- /database/migrations/2019_15_12_112000_create_elastic_migrations_table.php: -------------------------------------------------------------------------------- 1 | table = config('elastic.migrations.database.table'); 14 | } 15 | 16 | public function up(): void 17 | { 18 | if (!Schema::hasTable($this->table)) { 19 | Schema::create($this->table, static function (Blueprint $table) { 20 | $table->string('migration')->primary(); 21 | $table->integer('batch'); 22 | }); 23 | } 24 | } 25 | 26 | public function down(): void 27 | { 28 | Schema::dropIfExists($this->table); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ide.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "$schema": "https://laravel-ide.com/schema/laravel-ide-v2.json", 4 | "codeGenerations": [ 5 | { 6 | "id": "babenkoivan.create-elastic-migration", 7 | "name": "Create Elastic Migration", 8 | "inputFilter": "elastic", 9 | "files": [ 10 | { 11 | "directory": "/elastic/migrations", 12 | "name": "${CURRENT_TIME|format:yyyy_MM_dd_HHmmss}_${INPUT_CLASS|className|snakeCase}.php", 13 | "template": { 14 | "type": "stub", 15 | "path": "src/Console/stubs/migration.blank.stub", 16 | "parameters": { 17 | "DummyClass": "${INPUT_FQN|className}" 18 | } 19 | } 20 | } 21 | ] 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /phpstan-baseline.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | - 4 | message: "#^Parameter \\#1 \\$path of method Elastic\\\\Migrations\\\\Filesystem\\\\MigrationStorage\\:\\:makeFilePath\\(\\) expects string, mixed given\\.$#" 5 | count: 1 6 | path: src/Filesystem/MigrationStorage.php 7 | 8 | - 9 | message: "#^Property Elastic\\\\Migrations\\\\Filesystem\\\\MigrationStorage\\:\\:\\$defaultPath \\(string\\) does not accept mixed\\.$#" 10 | count: 1 11 | path: src/Filesystem/MigrationStorage.php 12 | 13 | - 14 | message: "#^Property Elastic\\\\Migrations\\\\Repositories\\\\MigrationRepository\\:\\:\\$connection \\(string\\|null\\) does not accept mixed\\.$#" 15 | count: 1 16 | path: src/Repositories/MigrationRepository.php 17 | 18 | - 19 | message: "#^Property Elastic\\\\Migrations\\\\Repositories\\\\MigrationRepository\\:\\:\\$table \\(string\\) does not accept mixed\\.$#" 20 | count: 1 21 | path: src/Repositories/MigrationRepository.php 22 | -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | includes: 2 | - phpstan-baseline.neon 3 | 4 | parameters: 5 | level: max 6 | paths: 7 | - src 8 | ignoreErrors: 9 | - identifier: missingType.iterableValue 10 | - identifier: missingType.generics 11 | - '#Parameter .+? of method Illuminate\\Support\\Collection<.+?>::.+?\(\) expects .+? given#' 12 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | tests/Unit 13 | 14 | 15 | tests/Integration 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | src 25 | 26 | 27 | src/ServiceProvider.php 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/Adapters/IndexManagerAdapter.php: -------------------------------------------------------------------------------- 1 | indexManager = $indexManager; 20 | } 21 | 22 | public function create(string $indexName, ?callable $modifier = null): IndexManagerInterface 23 | { 24 | $prefixedIndexName = prefix_index_name($indexName); 25 | 26 | if (isset($modifier)) { 27 | $mapping = new Mapping(); 28 | $settings = new Settings(); 29 | 30 | $modifier($mapping, $settings); 31 | 32 | $index = new Index($prefixedIndexName, $mapping, $settings); 33 | } else { 34 | $index = new Index($prefixedIndexName); 35 | } 36 | 37 | $this->indexManager->create($index); 38 | 39 | return $this; 40 | } 41 | 42 | public function createRaw(string $indexName, ?array $mapping = null, ?array $settings = null): IndexManagerInterface 43 | { 44 | $prefixedIndexName = prefix_index_name($indexName); 45 | 46 | $this->indexManager->createRaw($prefixedIndexName, $mapping, $settings); 47 | 48 | return $this; 49 | } 50 | 51 | public function createIfNotExists(string $indexName, ?callable $modifier = null): IndexManagerInterface 52 | { 53 | $prefixedIndexName = prefix_index_name($indexName); 54 | 55 | if (!$this->indexManager->exists($prefixedIndexName)) { 56 | $this->create($indexName, $modifier); 57 | } 58 | 59 | return $this; 60 | } 61 | 62 | public function createIfNotExistsRaw( 63 | string $indexName, 64 | ?array $mapping = null, 65 | ?array $settings = null 66 | ): IndexManagerInterface { 67 | $prefixedIndexName = prefix_index_name($indexName); 68 | 69 | if (!$this->indexManager->exists($prefixedIndexName)) { 70 | $this->createRaw($indexName, $mapping, $settings); 71 | } 72 | 73 | return $this; 74 | } 75 | 76 | public function putMapping(string $indexName, callable $modifier): IndexManagerInterface 77 | { 78 | $prefixedIndexName = prefix_index_name($indexName); 79 | 80 | $mapping = new Mapping(); 81 | $modifier($mapping); 82 | 83 | $this->indexManager->putMapping($prefixedIndexName, $mapping); 84 | 85 | return $this; 86 | } 87 | 88 | public function putMappingRaw(string $indexName, array $mapping): IndexManagerInterface 89 | { 90 | $prefixedIndexName = prefix_index_name($indexName); 91 | 92 | $this->indexManager->putMappingRaw($prefixedIndexName, $mapping); 93 | 94 | return $this; 95 | } 96 | 97 | public function putSettings(string $indexName, callable $modifier): IndexManagerInterface 98 | { 99 | $prefixedIndexName = prefix_index_name($indexName); 100 | 101 | $settings = new Settings(); 102 | $modifier($settings); 103 | 104 | $this->indexManager->putSettings($prefixedIndexName, $settings); 105 | 106 | return $this; 107 | } 108 | 109 | public function putSettingsRaw(string $indexName, array $settings): IndexManagerInterface 110 | { 111 | $prefixedIndexName = prefix_index_name($indexName); 112 | 113 | $this->indexManager->putSettingsRaw($prefixedIndexName, $settings); 114 | 115 | return $this; 116 | } 117 | 118 | public function pushSettings(string $indexName, callable $modifier): IndexManagerInterface 119 | { 120 | $prefixedIndexName = prefix_index_name($indexName); 121 | 122 | $this->indexManager->close($prefixedIndexName); 123 | $this->putSettings($indexName, $modifier); 124 | $this->indexManager->open($prefixedIndexName); 125 | 126 | return $this; 127 | } 128 | 129 | public function pushSettingsRaw(string $indexName, array $settings): IndexManagerInterface 130 | { 131 | $prefixedIndexName = prefix_index_name($indexName); 132 | 133 | $this->indexManager->close($prefixedIndexName); 134 | $this->putSettingsRaw($indexName, $settings); 135 | $this->indexManager->open($prefixedIndexName); 136 | 137 | return $this; 138 | } 139 | 140 | public function drop(string $indexName): IndexManagerInterface 141 | { 142 | $prefixedIndexName = prefix_index_name($indexName); 143 | 144 | $this->indexManager->drop($prefixedIndexName); 145 | 146 | return $this; 147 | } 148 | 149 | public function dropIfExists(string $indexName): IndexManagerInterface 150 | { 151 | $prefixedIndexName = prefix_index_name($indexName); 152 | 153 | if ($this->indexManager->exists($prefixedIndexName)) { 154 | $this->drop($indexName); 155 | } 156 | 157 | return $this; 158 | } 159 | 160 | public function putAlias(string $indexName, string $aliasName, ?array $settings = null): IndexManagerInterface 161 | { 162 | $prefixedIndexName = prefix_index_name($indexName); 163 | $prefixedAliasName = prefix_alias_name($aliasName); 164 | 165 | $this->indexManager->putAliasRaw($prefixedIndexName, $prefixedAliasName, $settings); 166 | 167 | return $this; 168 | } 169 | 170 | public function deleteAlias(string $indexName, string $aliasName): IndexManagerInterface 171 | { 172 | $prefixedIndexName = prefix_index_name($indexName); 173 | $prefixedAliasName = prefix_alias_name($aliasName); 174 | 175 | $this->indexManager->deleteAlias($prefixedIndexName, $prefixedAliasName); 176 | 177 | return $this; 178 | } 179 | 180 | public function connection(string $connection): IndexManagerInterface 181 | { 182 | $self = clone $this; 183 | $self->indexManager = $self->indexManager->connection($connection); 184 | return $self; 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/Console/FreshCommand.php: -------------------------------------------------------------------------------- 1 | setOutput($this->output); 31 | 32 | if (!$this->confirmToProceed() || !$migrator->isReady()) { 33 | return 1; 34 | } 35 | 36 | $indexManager->drop('*'); 37 | $migrationRepository->purge(); 38 | $migrator->migrateAll(); 39 | 40 | return 0; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Console/MakeCommand.php: -------------------------------------------------------------------------------- 1 | argument('name'); 27 | $fileName = sprintf('%s_%s', (new Carbon())->format('Y_m_d_His'), Str::snake(trim($name))); 28 | $className = Str::studly(trim($name)); 29 | 30 | $stub = $filesystem->get(__DIR__ . '/stubs/migration.blank.stub'); 31 | $content = str_replace('DummyClass', $className, $stub); 32 | 33 | $migrationStorage->create($fileName, $content); 34 | 35 | $this->output->writeln('Created migration: ' . $fileName); 36 | 37 | return 0; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Console/MigrateCommand.php: -------------------------------------------------------------------------------- 1 | setOutput($this->output); 27 | 28 | if (!$this->confirmToProceed() || !$migrator->isReady()) { 29 | return 1; 30 | } 31 | 32 | /** @var ?string $name */ 33 | $name = $this->argument('name'); 34 | 35 | if (isset($name)) { 36 | $migrator->migrateOne(trim($name)); 37 | } else { 38 | $migrator->migrateAll(); 39 | } 40 | 41 | return 0; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Console/RefreshCommand.php: -------------------------------------------------------------------------------- 1 | setOutput($this->output); 26 | 27 | if (!$this->confirmToProceed() || !$migrator->isReady()) { 28 | return 1; 29 | } 30 | 31 | $migrator->rollbackAll(); 32 | $migrator->migrateAll(); 33 | 34 | return 0; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Console/ResetCommand.php: -------------------------------------------------------------------------------- 1 | setOutput($this->output); 26 | 27 | if (!$this->confirmToProceed() || !$migrator->isReady()) { 28 | return 1; 29 | } 30 | 31 | $migrator->rollbackAll(); 32 | 33 | return 0; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Console/RollbackCommand.php: -------------------------------------------------------------------------------- 1 | setOutput($this->output); 27 | 28 | if (!$this->confirmToProceed() || !$migrator->isReady()) { 29 | return 1; 30 | } 31 | 32 | /** @var ?string $name */ 33 | $name = $this->argument('name'); 34 | 35 | if (isset($name)) { 36 | $migrator->rollbackOne(trim($name)); 37 | } else { 38 | $migrator->rollbackLastBatch(); 39 | } 40 | 41 | return 0; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Console/StatusCommand.php: -------------------------------------------------------------------------------- 1 | setOutput($this->output); 22 | 23 | if (!$migrator->isReady()) { 24 | return 1; 25 | } 26 | 27 | $onlyPending = (bool)$this->option('pending'); 28 | $migrator->showStatus($onlyPending); 29 | 30 | return 0; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Console/stubs/migration.blank.stub: -------------------------------------------------------------------------------- 1 | path(); 14 | 15 | $className = Str::studly(implode('_', array_slice(explode('_', $file->name()), 4))); 16 | /** @var MigrationInterface $migration */ 17 | $migration = resolve($className); 18 | 19 | return $migration; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Filesystem/MigrationFile.php: -------------------------------------------------------------------------------- 1 | filePath = $filePath; 14 | } 15 | 16 | public function name(): string 17 | { 18 | return basename($this->filePath, static::FILE_EXTENSION); 19 | } 20 | 21 | public function path(): string 22 | { 23 | return $this->filePath; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Filesystem/MigrationStorage.php: -------------------------------------------------------------------------------- 1 | filesystem = $filesystem; 21 | $this->defaultPath = config('elastic.migrations.storage.default_path', ''); 22 | $this->paths = collect([$this->defaultPath]); 23 | } 24 | 25 | public function create(string $fileName, string $content): MigrationFile 26 | { 27 | if ($this->isPath($fileName)) { 28 | $this->filesystem->put($fileName, $content); 29 | return new MigrationFile($fileName); 30 | } 31 | 32 | if (!$this->filesystem->isDirectory($this->defaultPath)) { 33 | $this->filesystem->makeDirectory($this->defaultPath, static::DIRECTORY_PERMISSIONS, true); 34 | } 35 | 36 | $filePath = $this->makeFilePath($this->defaultPath, $fileName); 37 | $this->filesystem->put($filePath, $content); 38 | return new MigrationFile($filePath); 39 | } 40 | 41 | public function whereName(string $fileName): ?MigrationFile 42 | { 43 | if ($this->isPath($fileName)) { 44 | return $this->filesystem->exists($fileName) ? new MigrationFile($fileName) : null; 45 | } 46 | 47 | foreach ($this->paths as $path) { 48 | $filePath = $this->makeFilePath($path, $fileName); 49 | 50 | if ($this->filesystem->exists($filePath)) { 51 | return new MigrationFile($filePath); 52 | } 53 | } 54 | 55 | return null; 56 | } 57 | 58 | public function all(): Collection 59 | { 60 | return $this->paths->flatMap( 61 | fn (string $path) => $this->filesystem->glob($path . '/*_*' . MigrationFile::FILE_EXTENSION) 62 | )->filter()->mapWithKeys( 63 | static function (string $filePath) { 64 | $file = new MigrationFile($filePath); 65 | return [$file->name() => $file]; 66 | } 67 | )->sortKeys()->values(); 68 | } 69 | 70 | public function registerPaths(array $paths): self 71 | { 72 | $this->paths = $this->paths->merge($paths)->filter()->unique()->values(); 73 | return $this; 74 | } 75 | 76 | public function isReady(): bool 77 | { 78 | return $this->filesystem->isDirectory($this->defaultPath); 79 | } 80 | 81 | private function isPath(string $path): bool 82 | { 83 | return strpos($path, DIRECTORY_SEPARATOR) !== false; 84 | } 85 | 86 | private function makeFilePath(string $path, string $fileName): string 87 | { 88 | return $path . DIRECTORY_SEPARATOR . $fileName . MigrationFile::FILE_EXTENSION; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/IndexManagerInterface.php: -------------------------------------------------------------------------------- 1 | migrationStorage = $migrationStorage; 25 | $this->migrationRepository = $migrationRepository; 26 | $this->migrationFactory = $migrationFactory; 27 | } 28 | 29 | public function setOutput(OutputStyle $output): self 30 | { 31 | $this->output = $output; 32 | return $this; 33 | } 34 | 35 | public function migrateOne(string $fileName): self 36 | { 37 | $file = $this->migrationStorage->whereName($fileName); 38 | 39 | if (is_null($file)) { 40 | $this->output->writeln('Migration is not found: ' . $fileName); 41 | } else { 42 | $this->migrate(collect([$file])); 43 | } 44 | 45 | return $this; 46 | } 47 | 48 | public function migrateAll(): self 49 | { 50 | $files = $this->migrationStorage->all(); 51 | $migratedFileNames = $this->migrationRepository->all(); 52 | 53 | $nonMigratedFiles = $files->filter( 54 | static fn (MigrationFile $file) => !$migratedFileNames->contains($file->name()) 55 | ); 56 | 57 | $this->migrate($nonMigratedFiles); 58 | 59 | return $this; 60 | } 61 | 62 | public function rollbackOne(string $fileName): self 63 | { 64 | $file = $this->migrationStorage->whereName($fileName); 65 | 66 | if (is_null($file)) { 67 | $this->output->writeln('Migration is not found: ' . $fileName); 68 | } elseif (!$this->migrationRepository->exists($file->name())) { 69 | $this->output->writeln('Migration is not yet migrated: ' . $file->name()); 70 | } else { 71 | $this->rollback(collect([$file->name()])); 72 | } 73 | 74 | return $this; 75 | } 76 | 77 | public function rollbackLastBatch(): self 78 | { 79 | $fileNames = $this->migrationRepository->lastBatch(); 80 | 81 | $this->rollback($fileNames); 82 | 83 | return $this; 84 | } 85 | 86 | public function rollbackAll(): self 87 | { 88 | $fileNames = $this->migrationRepository->all(); 89 | 90 | $this->rollback($fileNames); 91 | 92 | return $this; 93 | } 94 | 95 | public function showStatus(bool $onlyPending = false): self 96 | { 97 | $files = $this->migrationStorage->all(); 98 | $migratedFiles = $this->migrationRepository->all(); 99 | $lastBatch = $this->migrationRepository->lastBatch(); 100 | 101 | $rows = $files->map( 102 | static fn (MigrationFile $file) => [ 103 | $file->name(), 104 | $migratedFiles->contains($file->name()) 105 | ? 'Ran' . ($lastBatch->contains($file->name()) ? ' (last batch)' : '') 106 | : 'Pending', 107 | ] 108 | )->when($onlyPending, static fn (Collection $rows) => $rows->filter( 109 | static fn (array $row) => strpos($row[1], 'Pending') !== false 110 | )->values())->toArray(); 111 | 112 | if ($rows !== []) { 113 | $headers = ['Migration name', 'Status']; 114 | $this->output->table($headers, $rows); 115 | return $this; 116 | } 117 | 118 | $this->output->writeln( 119 | sprintf('%s', $onlyPending ? 'No pending migrations' : 'No migrations found') 120 | ); 121 | 122 | return $this; 123 | } 124 | 125 | private function migrate(Collection $files): self 126 | { 127 | if ($files->isEmpty()) { 128 | $this->output->writeln('Nothing to migrate'); 129 | return $this; 130 | } 131 | 132 | $nextBatchNumber = $this->migrationRepository->lastBatchNumber() + 1; 133 | 134 | $files->each(function (MigrationFile $file) use ($nextBatchNumber) { 135 | $this->output->writeln('Migrating: ' . $file->name()); 136 | 137 | $migration = $this->migrationFactory->makeFromFile($file); 138 | $migration->up(); 139 | 140 | $this->migrationRepository->insert($file->name(), $nextBatchNumber); 141 | 142 | $this->output->writeln('Migrated: ' . $file->name()); 143 | }); 144 | 145 | return $this; 146 | } 147 | 148 | private function rollback(Collection $fileNames): self 149 | { 150 | $files = $fileNames->map( 151 | fn (string $fileName) => $this->migrationStorage->whereName($fileName) 152 | )->filter(); 153 | 154 | if ($fileNames->isEmpty()) { 155 | $this->output->writeln('Nothing to roll back'); 156 | return $this; 157 | } 158 | 159 | if ($fileNames->count() !== $files->count()) { 160 | $this->output->writeln( 161 | 'Migration is not found: ' . 162 | implode( 163 | ',', 164 | $fileNames->diff($files->map(static fn (MigrationFile $file) => $file->name()))->toArray() 165 | ) 166 | ); 167 | 168 | return $this; 169 | } 170 | 171 | $files->each(function (MigrationFile $file) { 172 | $this->output->writeln('Rolling back: ' . $file->name()); 173 | 174 | $migration = $this->migrationFactory->makeFromFile($file); 175 | $migration->down(); 176 | 177 | $this->migrationRepository->delete($file->name()); 178 | 179 | $this->output->writeln('Rolled back: ' . $file->name()); 180 | }); 181 | 182 | return $this; 183 | } 184 | 185 | public function isReady(): bool 186 | { 187 | if (!$isMigrationRepositoryReady = $this->migrationRepository->isReady()) { 188 | $this->output->writeln('Migration table is not yet created'); 189 | } 190 | 191 | if (!$isMigrationStorageReady = $this->migrationStorage->isReady()) { 192 | $this->output->writeln('Default migration path is not yet created'); 193 | } 194 | 195 | return $isMigrationRepositoryReady && $isMigrationStorageReady; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/ReadinessInterface.php: -------------------------------------------------------------------------------- 1 | table = config('elastic.migrations.database.table'); 20 | $this->connection = config('elastic.migrations.database.connection'); 21 | } 22 | 23 | public function insert(string $fileName, int $batchNumber): bool 24 | { 25 | return $this->table()->insert([ 26 | 'migration' => $fileName, 27 | 'batch' => $batchNumber, 28 | ]); 29 | } 30 | 31 | public function exists(string $fileName): bool 32 | { 33 | return $this->table() 34 | ->where('migration', $fileName) 35 | ->exists(); 36 | } 37 | 38 | public function delete(string $fileName): bool 39 | { 40 | return (bool)$this->table() 41 | ->where('migration', $fileName) 42 | ->delete(); 43 | } 44 | 45 | public function purge(): void 46 | { 47 | $this->table()->delete(); 48 | } 49 | 50 | public function lastBatchNumber(): ?int 51 | { 52 | /** @var stdClass|null $record */ 53 | $record = $this->table() 54 | ->select('batch') 55 | ->orderBy('batch', 'desc') 56 | ->first(); 57 | 58 | return isset($record) ? (int)$record->batch : null; 59 | } 60 | 61 | public function lastBatch(): Collection 62 | { 63 | return $this->table() 64 | ->where('batch', $this->lastBatchNumber()) 65 | ->orderBy('migration', 'desc') 66 | ->pluck('migration'); 67 | } 68 | 69 | public function all(): Collection 70 | { 71 | return $this->table() 72 | ->orderBy('migration', 'desc') 73 | ->pluck('migration'); 74 | } 75 | 76 | public function isReady(): bool 77 | { 78 | return Schema::connection($this->connection)->hasTable($this->table); 79 | } 80 | 81 | private function table(): Builder 82 | { 83 | return DB::connection($this->connection)->table($this->table); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | configPath = dirname(__DIR__) . '/config/elastic.migrations.php'; 39 | $this->migrationsPath = dirname(__DIR__) . '/database/migrations'; 40 | } 41 | 42 | /** 43 | * {@inheritDoc} 44 | */ 45 | public function register() 46 | { 47 | $this->mergeConfigFrom( 48 | $this->configPath, 49 | basename($this->configPath, '.php') 50 | ); 51 | 52 | $this->app->singletonIf(MigrationStorage::class); 53 | $this->app->bindIf(IndexManagerInterface::class, IndexManagerAdapter::class); 54 | } 55 | 56 | /** 57 | * @return void 58 | */ 59 | public function boot() 60 | { 61 | $this->publishes([ 62 | $this->configPath => config_path(basename($this->configPath)), 63 | ]); 64 | 65 | $this->loadMigrationsFrom($this->migrationsPath); 66 | 67 | $this->commands($this->commands); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/helpers.php: -------------------------------------------------------------------------------- 1 | indexManagerMock = $this->createMock(IndexManager::class); 26 | $this->indexManagerAdapter = new IndexManagerAdapter($this->indexManagerMock); 27 | } 28 | 29 | #[DataProvider('prefixProvider')] 30 | public function test_index_can_be_created_without_modifier(string $indexNamePrefix): void 31 | { 32 | $this->config->set('elastic.migrations.prefixes.index', $indexNamePrefix); 33 | 34 | $indexName = 'test'; 35 | 36 | $this->indexManagerMock 37 | ->expects($this->once()) 38 | ->method('create') 39 | ->with(new Index($indexNamePrefix . $indexName)); 40 | 41 | $this->indexManagerAdapter->create($indexName); 42 | } 43 | 44 | #[DataProvider('prefixProvider')] 45 | public function test_index_can_be_created_with_modifier(string $indexNamePrefix): void 46 | { 47 | $this->config->set('elastic.migrations.prefixes.index', $indexNamePrefix); 48 | 49 | $indexName = 'test'; 50 | 51 | $modifier = static function (Mapping $mapping, Settings $settings) { 52 | $mapping->text('title'); 53 | $settings->index(['number_of_replicas' => 2]); 54 | }; 55 | 56 | $this->indexManagerMock 57 | ->expects($this->once()) 58 | ->method('create') 59 | ->with(new Index( 60 | $indexNamePrefix . $indexName, 61 | (new Mapping())->text('title'), 62 | (new Settings())->index(['number_of_replicas' => 2]) 63 | )); 64 | 65 | $this->indexManagerAdapter->create($indexName, $modifier); 66 | } 67 | 68 | #[DataProvider('prefixProvider')] 69 | public function test_index_can_be_created_with_raw_mapping(string $indexNamePrefix): void 70 | { 71 | $this->config->set('elastic.migrations.prefixes.index', $indexNamePrefix); 72 | 73 | $indexName = 'test'; 74 | 75 | $mapping = [ 76 | 'properties' => [ 77 | 'title' => [ 78 | 'type' => 'text', 79 | ], 80 | ], 81 | ]; 82 | 83 | $this->indexManagerMock 84 | ->expects($this->once()) 85 | ->method('createRaw') 86 | ->with($indexNamePrefix . $indexName, $mapping); 87 | 88 | $this->indexManagerAdapter->createRaw($indexName, $mapping); 89 | } 90 | 91 | #[DataProvider('prefixProvider')] 92 | public function test_index_with_modifier_can_be_created_only_if_it_does_not_exist(string $indexNamePrefix): void 93 | { 94 | $this->config->set('elastic.migrations.prefixes.index', $indexNamePrefix); 95 | 96 | $indexName = 'test'; 97 | 98 | $this->indexManagerMock 99 | ->expects($this->once()) 100 | ->method('exists') 101 | ->with($indexNamePrefix . $indexName) 102 | ->willReturn(false); 103 | 104 | $this->indexManagerMock 105 | ->expects($this->once()) 106 | ->method('create') 107 | ->with(new Index($indexNamePrefix . $indexName)); 108 | 109 | $this->indexManagerAdapter->createIfNotExists($indexName); 110 | } 111 | 112 | #[DataProvider('prefixProvider')] 113 | public function test_index_with_raw_mapping_can_be_created_only_if_it_does_not_exist(string $indexNamePrefix): void 114 | { 115 | $this->config->set('elastic.migrations.prefixes.index', $indexNamePrefix); 116 | 117 | $indexName = 'test'; 118 | 119 | $mapping = [ 120 | 'properties' => [ 121 | 'title' => [ 122 | 'type' => 'text', 123 | ], 124 | ], 125 | ]; 126 | 127 | $this->indexManagerMock 128 | ->expects($this->once()) 129 | ->method('exists') 130 | ->with($indexNamePrefix . $indexName) 131 | ->willReturn(false); 132 | 133 | $this->indexManagerMock 134 | ->expects($this->once()) 135 | ->method('createRaw') 136 | ->with($indexNamePrefix . $indexName, $mapping); 137 | 138 | $this->indexManagerAdapter->createIfNotExistsRaw($indexName, $mapping); 139 | } 140 | 141 | #[DataProvider('prefixProvider')] 142 | public function test_mapping_can_be_updated_using_modifier(string $indexNamePrefix): void 143 | { 144 | $this->config->set('elastic.migrations.prefixes.index', $indexNamePrefix); 145 | 146 | $indexName = 'test'; 147 | 148 | $modifier = static function (Mapping $mapping) { 149 | $mapping->disableSource()->text('title'); 150 | }; 151 | 152 | $this->indexManagerMock 153 | ->expects($this->once()) 154 | ->method('putMapping') 155 | ->with( 156 | $indexNamePrefix . $indexName, 157 | (new Mapping())->disableSource()->text('title') 158 | ); 159 | 160 | $this->indexManagerAdapter->putMapping($indexName, $modifier); 161 | } 162 | 163 | #[DataProvider('prefixProvider')] 164 | public function test_mapping_can_be_updated_using_raw_input(string $indexNamePrefix): void 165 | { 166 | $this->config->set('elastic.migrations.prefixes.index', $indexNamePrefix); 167 | 168 | $indexName = 'test'; 169 | 170 | $mapping = [ 171 | 'properties' => [ 172 | 'title' => ['type' => 'text'], 173 | ], 174 | ]; 175 | 176 | $this->indexManagerMock 177 | ->expects($this->once()) 178 | ->method('putMappingRaw') 179 | ->with($indexNamePrefix . $indexName, $mapping); 180 | 181 | $this->indexManagerAdapter->putMappingRaw($indexName, $mapping); 182 | } 183 | 184 | #[DataProvider('prefixProvider')] 185 | public function test_settings_can_be_updated_using_modifier(string $indexNamePrefix): void 186 | { 187 | $this->config->set('elastic.migrations.prefixes.index', $indexNamePrefix); 188 | 189 | $indexName = 'test'; 190 | 191 | $modifier = static function (Settings $settings) { 192 | $settings->index(['number_of_replicas' => 2, 'refresh_interval' => -1]); 193 | }; 194 | 195 | $this->indexManagerMock 196 | ->expects($this->once()) 197 | ->method('putSettings') 198 | ->with( 199 | $indexNamePrefix . $indexName, 200 | (new Settings())->index(['number_of_replicas' => 2, 'refresh_interval' => -1]) 201 | ); 202 | 203 | $this->indexManagerAdapter->putSettings($indexName, $modifier); 204 | } 205 | 206 | #[DataProvider('prefixProvider')] 207 | public function test_settings_can_be_updated_using_raw_input(string $indexNamePrefix): void 208 | { 209 | $this->config->set('elastic.migrations.prefixes.index', $indexNamePrefix); 210 | 211 | $indexName = 'test'; 212 | $settings = ['number_of_replicas' => 2]; 213 | 214 | $this->indexManagerMock 215 | ->expects($this->once()) 216 | ->method('putSettingsRaw') 217 | ->with($indexNamePrefix . $indexName, $settings); 218 | 219 | $this->indexManagerAdapter->putSettingsRaw($indexName, $settings); 220 | } 221 | 222 | #[DataProvider('prefixProvider')] 223 | public function test_settings_can_be_pushed_using_modifier(string $indexNamePrefix): void 224 | { 225 | $this->config->set('elastic.migrations.prefixes.index', $indexNamePrefix); 226 | 227 | $indexName = 'test'; 228 | 229 | $modifier = static function (Settings $settings) { 230 | $settings->index(['number_of_replicas' => 2]); 231 | }; 232 | 233 | $this->indexManagerMock 234 | ->expects($this->once()) 235 | ->method('close') 236 | ->with($indexNamePrefix . $indexName); 237 | 238 | $this->indexManagerMock 239 | ->expects($this->once()) 240 | ->method('putSettings') 241 | ->with( 242 | $indexNamePrefix . $indexName, 243 | (new Settings())->index(['number_of_replicas' => 2]) 244 | ); 245 | 246 | $this->indexManagerMock 247 | ->expects($this->once()) 248 | ->method('open') 249 | ->with($indexNamePrefix . $indexName); 250 | 251 | $this->indexManagerAdapter->pushSettings($indexName, $modifier); 252 | } 253 | 254 | #[DataProvider('prefixProvider')] 255 | public function test_settings_can_be_pushed_using_raw_input(string $indexNamePrefix): void 256 | { 257 | $this->config->set('elastic.migrations.prefixes.index', $indexNamePrefix); 258 | 259 | $indexName = 'test'; 260 | $settings = ['number_of_replicas' => 2]; 261 | 262 | $this->indexManagerMock 263 | ->expects($this->once()) 264 | ->method('close') 265 | ->with($indexNamePrefix . $indexName); 266 | 267 | $this->indexManagerMock 268 | ->expects($this->once()) 269 | ->method('putSettingsRaw') 270 | ->with($indexNamePrefix . $indexName, $settings); 271 | 272 | $this->indexManagerMock 273 | ->expects($this->once()) 274 | ->method('open') 275 | ->with($indexNamePrefix . $indexName); 276 | 277 | $this->indexManagerAdapter->pushSettingsRaw($indexName, $settings); 278 | } 279 | 280 | #[DataProvider('prefixProvider')] 281 | public function test_index_can_be_dropped(string $indexNamePrefix): void 282 | { 283 | $this->config->set('elastic.migrations.prefixes.index', $indexNamePrefix); 284 | 285 | $indexName = 'test'; 286 | 287 | $this->indexManagerMock 288 | ->expects($this->once()) 289 | ->method('drop') 290 | ->with($indexNamePrefix . $indexName); 291 | 292 | $this->indexManagerAdapter->drop($indexName); 293 | } 294 | 295 | #[DataProvider('prefixProvider')] 296 | public function test_index_can_be_dropped_only_if_exists(string $indexNamePrefix): void 297 | { 298 | $this->config->set('elastic.migrations.prefixes.index', $indexNamePrefix); 299 | 300 | $indexName = 'test'; 301 | 302 | $this->indexManagerMock 303 | ->expects($this->once()) 304 | ->method('exists') 305 | ->with($indexNamePrefix . $indexName) 306 | ->willReturn(true); 307 | 308 | $this->indexManagerMock 309 | ->expects($this->once()) 310 | ->method('drop') 311 | ->with($indexNamePrefix . $indexName); 312 | 313 | $this->indexManagerAdapter->dropIfExists($indexName); 314 | } 315 | 316 | #[DataProvider('prefixProvider')] 317 | public function test_alias_can_be_created(string $aliasNamePrefix): void 318 | { 319 | $this->config->set('elastic.migrations.prefixes.alias', $aliasNamePrefix); 320 | 321 | $indexName = 'foo'; 322 | $aliasName = 'bar'; 323 | 324 | $this->indexManagerMock 325 | ->expects($this->once()) 326 | ->method('putAliasRaw') 327 | ->with($indexName, $aliasNamePrefix . $aliasName); 328 | 329 | $this->indexManagerAdapter->putAlias($indexName, $aliasName); 330 | } 331 | 332 | #[DataProvider('prefixProvider')] 333 | public function test_alias_can_be_deleted(string $aliasNamePrefix): void 334 | { 335 | $this->config->set('elastic.migrations.prefixes.alias', $aliasNamePrefix); 336 | 337 | $indexName = 'foo'; 338 | $aliasName = 'bar'; 339 | 340 | $this->indexManagerMock 341 | ->expects($this->once()) 342 | ->method('deleteAlias') 343 | ->with($indexName, $aliasNamePrefix . $aliasName); 344 | 345 | $this->indexManagerAdapter->deleteAlias($indexName, $aliasName); 346 | } 347 | 348 | public function test_connection_can_be_changed(): void 349 | { 350 | $connection = 'test'; 351 | 352 | $this->indexManagerMock 353 | ->expects($this->once()) 354 | ->method('connection') 355 | ->with($connection); 356 | 357 | $this->indexManagerAdapter->connection($connection); 358 | } 359 | 360 | public static function prefixProvider(): array 361 | { 362 | return [ 363 | 'no prefix' => [''], 364 | 'short prefix' => ['foo_'], 365 | 'long prefix' => ['foo_bar_'], 366 | ]; 367 | } 368 | } 369 | -------------------------------------------------------------------------------- /tests/Integration/Console/FreshCommandTest.php: -------------------------------------------------------------------------------- 1 | migrator = $this->createMock(Migrator::class); 28 | $this->app->instance(Migrator::class, $this->migrator); 29 | 30 | $this->migrationRepository = $this->createMock(MigrationRepository::class); 31 | $this->app->instance(MigrationRepository::class, $this->migrationRepository); 32 | 33 | $this->indexManager = $this->createMock(IndexManagerInterface::class); 34 | $this->app->instance(IndexManagerInterface::class, $this->indexManager); 35 | 36 | $this->command = new FreshCommand(); 37 | $this->command->setLaravel($this->app); 38 | } 39 | 40 | public function test_does_nothing_if_migrator_is_not_ready(): void 41 | { 42 | $this->migrator 43 | ->expects($this->once()) 44 | ->method('isReady') 45 | ->willReturn(false); 46 | 47 | $this->indexManager 48 | ->expects($this->never()) 49 | ->method('drop'); 50 | 51 | $this->migrationRepository 52 | ->expects($this->never()) 53 | ->method('purge'); 54 | 55 | $this->migrator 56 | ->expects($this->never()) 57 | ->method('migrateAll'); 58 | 59 | $result = $this->command->run( 60 | new ArrayInput(['--force' => true]), 61 | new NullOutput() 62 | ); 63 | 64 | $this->assertSame(1, $result); 65 | } 66 | 67 | public function test_drops_indices_and_migration(): void 68 | { 69 | $this->migrator 70 | ->expects($this->once()) 71 | ->method('isReady') 72 | ->willReturn(true); 73 | 74 | $this->indexManager 75 | ->expects($this->once()) 76 | ->method('drop') 77 | ->with('*'); 78 | 79 | $this->migrationRepository 80 | ->expects($this->once()) 81 | ->method('purge'); 82 | 83 | $this->migrator 84 | ->expects($this->once()) 85 | ->method('migrateAll'); 86 | 87 | $result = $this->command->run( 88 | new ArrayInput(['--force' => true]), 89 | new NullOutput() 90 | ); 91 | 92 | $this->assertSame(0, $result); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/Integration/Console/MakeCommandTest.php: -------------------------------------------------------------------------------- 1 | createMock(MigrationStorage::class); 18 | $this->app->instance(MigrationStorage::class, $migrationStorageMock); 19 | 20 | /** @var string $migrationStub */ 21 | $migrationStub = file_get_contents(dirname(__DIR__, 3) . '/src/Console/stubs/migration.blank.stub'); 22 | 23 | $migrationStorageMock 24 | ->expects($this->once()) 25 | ->method('create') 26 | ->with( 27 | $this->stringEndsWith('_test_migration_creation'), 28 | str_replace('DummyClass', 'TestMigrationCreation', $migrationStub) 29 | ); 30 | 31 | $command = new MakeCommand(); 32 | $command->setLaravel($this->app); 33 | 34 | $input = new ArrayInput(['name' => 'test_migration_creation']); 35 | $output = new BufferedOutput(); 36 | 37 | $resultCode = $command->run($input, $output); 38 | $resultMessage = $output->fetch(); 39 | 40 | $this->assertSame(0, $resultCode); 41 | 42 | $this->assertStringContainsString('Created migration', $resultMessage); 43 | $this->assertStringContainsString('_test_migration_creation', $resultMessage); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/Integration/Console/MigrateCommandTest.php: -------------------------------------------------------------------------------- 1 | migrator = $this->createMock(Migrator::class); 24 | $this->app->instance(Migrator::class, $this->migrator); 25 | 26 | $this->command = new MigrateCommand(); 27 | $this->command->setLaravel($this->app); 28 | } 29 | 30 | public function test_does_nothing_if_migrator_is_not_ready(): void 31 | { 32 | $this->migrator 33 | ->expects($this->once()) 34 | ->method('isReady') 35 | ->willReturn(false); 36 | 37 | $this->migrator 38 | ->expects($this->never()) 39 | ->method('migrateOne'); 40 | 41 | $this->migrator 42 | ->expects($this->never()) 43 | ->method('migrateAll'); 44 | 45 | $result = $this->command->run( 46 | new ArrayInput(['--force' => true]), 47 | new NullOutput() 48 | ); 49 | 50 | $this->assertSame(1, $result); 51 | } 52 | 53 | public function test_runs_one_migration_if_file_name_is_provided(): void 54 | { 55 | $this->migrator 56 | ->expects($this->once()) 57 | ->method('isReady') 58 | ->willReturn(true); 59 | 60 | $this->migrator 61 | ->expects($this->once()) 62 | ->method('migrateOne') 63 | ->with('test_file_name'); 64 | 65 | $result = $this->command->run( 66 | new ArrayInput(['--force' => true, 'name' => 'test_file_name']), 67 | new NullOutput() 68 | ); 69 | 70 | $this->assertSame(0, $result); 71 | } 72 | 73 | public function test_runs_all_migrations_if_file_name_is_not_provided(): void 74 | { 75 | $this->migrator 76 | ->expects($this->once()) 77 | ->method('isReady') 78 | ->willReturn(true); 79 | 80 | $this->migrator 81 | ->expects($this->once()) 82 | ->method('migrateAll'); 83 | 84 | $result = $this->command->run( 85 | new ArrayInput(['--force' => true]), 86 | new NullOutput() 87 | ); 88 | 89 | $this->assertSame(0, $result); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tests/Integration/Console/RefreshCommandTest.php: -------------------------------------------------------------------------------- 1 | migrator = $this->createMock(Migrator::class); 24 | $this->app->instance(Migrator::class, $this->migrator); 25 | 26 | $this->command = new RefreshCommand(); 27 | $this->command->setLaravel($this->app); 28 | } 29 | 30 | public function test_does_nothing_if_migrator_is_not_ready(): void 31 | { 32 | $this->migrator 33 | ->expects($this->once()) 34 | ->method('isReady') 35 | ->willReturn(false); 36 | 37 | $this->migrator 38 | ->expects($this->never()) 39 | ->method('rollbackAll'); 40 | 41 | $this->migrator 42 | ->expects($this->never()) 43 | ->method('migrateAll'); 44 | 45 | $result = $this->command->run( 46 | new ArrayInput(['--force' => true]), 47 | new NullOutput() 48 | ); 49 | 50 | $this->assertSame(1, $result); 51 | } 52 | 53 | public function test_resets_and_reruns_all_migrations_if_migrator_is_ready(): void 54 | { 55 | $this->migrator 56 | ->expects($this->once()) 57 | ->method('isReady') 58 | ->willReturn(true); 59 | 60 | $this->migrator 61 | ->expects($this->once()) 62 | ->method('rollbackAll'); 63 | 64 | $this->migrator 65 | ->expects($this->once()) 66 | ->method('migrateAll'); 67 | 68 | $result = $this->command->run( 69 | new ArrayInput(['--force' => true]), 70 | new NullOutput() 71 | ); 72 | 73 | $this->assertSame(0, $result); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tests/Integration/Console/ResetCommandTest.php: -------------------------------------------------------------------------------- 1 | migrator = $this->createMock(Migrator::class); 24 | $this->app->instance(Migrator::class, $this->migrator); 25 | 26 | $this->command = new ResetCommand(); 27 | $this->command->setLaravel($this->app); 28 | } 29 | 30 | public function test_does_nothing_if_migrator_is_not_ready(): void 31 | { 32 | $this->migrator 33 | ->expects($this->once()) 34 | ->method('isReady') 35 | ->willReturn(false); 36 | 37 | $this->migrator 38 | ->expects($this->never()) 39 | ->method('rollbackAll'); 40 | 41 | $result = $this->command->run( 42 | new ArrayInput(['--force' => true]), 43 | new NullOutput() 44 | ); 45 | 46 | $this->assertSame(1, $result); 47 | } 48 | 49 | public function test_rollbacks_all_migrations_if_migrator_is_ready(): void 50 | { 51 | $this->migrator 52 | ->expects($this->once()) 53 | ->method('isReady') 54 | ->willReturn(true); 55 | 56 | $this->migrator 57 | ->expects($this->once()) 58 | ->method('rollbackAll'); 59 | 60 | $result = $this->command->run( 61 | new ArrayInput(['--force' => true]), 62 | new NullOutput() 63 | ); 64 | 65 | $this->assertSame(0, $result); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/Integration/Console/RollbackCommandTest.php: -------------------------------------------------------------------------------- 1 | migrator = $this->createMock(Migrator::class); 24 | $this->app->instance(Migrator::class, $this->migrator); 25 | 26 | $this->command = new RollbackCommand(); 27 | $this->command->setLaravel($this->app); 28 | } 29 | 30 | public function test_does_nothing_if_migrator_is_not_ready(): void 31 | { 32 | $this->migrator 33 | ->expects($this->once()) 34 | ->method('isReady') 35 | ->willReturn(false); 36 | 37 | $this->migrator 38 | ->expects($this->never()) 39 | ->method('rollbackOne'); 40 | 41 | $this->migrator 42 | ->expects($this->never()) 43 | ->method('rollbackLastBatch'); 44 | 45 | $result = $this->command->run( 46 | new ArrayInput(['--force' => true]), 47 | new NullOutput() 48 | ); 49 | 50 | $this->assertSame(1, $result); 51 | } 52 | 53 | public function test_rollbacks_one_migration_if_file_name_is_provided(): void 54 | { 55 | $this->migrator 56 | ->expects($this->once()) 57 | ->method('isReady') 58 | ->willReturn(true); 59 | 60 | $this->migrator 61 | ->expects($this->once()) 62 | ->method('rollbackOne') 63 | ->with('test_file_name'); 64 | 65 | $result = $this->command->run( 66 | new ArrayInput(['--force' => true, 'name' => 'test_file_name']), 67 | new NullOutput() 68 | ); 69 | 70 | $this->assertSame(0, $result); 71 | } 72 | 73 | public function test_rollbacks_last_batch_if_file_name_is_not_provided(): void 74 | { 75 | $this->migrator 76 | ->expects($this->once()) 77 | ->method('isReady') 78 | ->willReturn(true); 79 | 80 | $this->migrator 81 | ->expects($this->once()) 82 | ->method('rollbackLastBatch'); 83 | 84 | $result = $this->command->run( 85 | new ArrayInput(['--force' => true]), 86 | new NullOutput() 87 | ); 88 | 89 | $this->assertSame(0, $result); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tests/Integration/Console/StatusCommandTest.php: -------------------------------------------------------------------------------- 1 | migrator = $this->createMock(Migrator::class); 24 | $this->app->instance(Migrator::class, $this->migrator); 25 | 26 | $this->command = new StatusCommand(); 27 | $this->command->setLaravel($this->app); 28 | } 29 | 30 | public function test_does_nothing_if_migrator_is_not_ready(): void 31 | { 32 | $this->migrator 33 | ->expects($this->once()) 34 | ->method('isReady') 35 | ->willReturn(false); 36 | 37 | $this->migrator 38 | ->expects($this->never()) 39 | ->method('showStatus'); 40 | 41 | $result = $this->command->run( 42 | new ArrayInput([]), 43 | new NullOutput() 44 | ); 45 | 46 | $this->assertSame(1, $result); 47 | } 48 | 49 | public function test_displays_each_migration_status_if_migrator_is_ready(): void 50 | { 51 | $this->migrator 52 | ->expects($this->once()) 53 | ->method('isReady') 54 | ->willReturn(true); 55 | 56 | $this->migrator 57 | ->expects($this->once()) 58 | ->method('showStatus'); 59 | 60 | $result = $this->command->run( 61 | new ArrayInput([]), 62 | new NullOutput() 63 | ); 64 | 65 | $this->assertSame(0, $result); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/Integration/Facades/IndexTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(IndexManagerInterface::class, Index::getFacadeRoot()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/Integration/Factories/MigrationFactoryTest.php: -------------------------------------------------------------------------------- 1 | migrationFactory = resolve(MigrationFactory::class); 24 | $this->migrationStorage = resolve(MigrationStorage::class); 25 | } 26 | 27 | #[TestWith(['2018_12_01_081000_create_test_index'])] 28 | #[TestWith(['2019_08_10_142230_update_test_index_mapping'])] 29 | public function test_migration_can_be_created_from_file(string $fileName): void 30 | { 31 | /** @var MigrationFile $file */ 32 | $file = $this->migrationStorage->whereName($fileName); 33 | 34 | $this->assertInstanceOf( 35 | MigrationInterface::class, 36 | $this->migrationFactory->makeFromFile($file) 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Integration/Filesystem/MigrationStorageTest.php: -------------------------------------------------------------------------------- 1 | migrationStorage = resolve(MigrationStorage::class); 21 | $this->migrationStorage->registerPaths([__DIR__ . '/../../migrations/archive']); 22 | } 23 | 24 | #[TestWith(['2022_06_01_223400_create_new_index'])] 25 | #[TestWith([__DIR__ . '/../../migrations/archive/2022_06_01_223400_create_new_index.php'])] 26 | public function test_file_can_be_created(string $fileName): void 27 | { 28 | $file = $this->migrationStorage->create($fileName, 'content'); 29 | 30 | $this->assertFileExists($file->path()); 31 | $this->assertStringEqualsFile($file->path(), 'content'); 32 | 33 | @unlink($file->path()); 34 | } 35 | 36 | public function test_directory_is_created_along_with_file(): void 37 | { 38 | $defaultPath = __DIR__ . '/../../migrations/tmp'; 39 | $this->config->set('elastic.migrations.storage.default_path', $defaultPath); 40 | 41 | // create a new instance to apply the new config 42 | $this->app->forgetInstance(MigrationStorage::class); 43 | $migrationStorage = resolve(MigrationStorage::class); 44 | 45 | $file = $migrationStorage->create('test', 'content'); 46 | 47 | $this->assertDirectoryExists($defaultPath); 48 | 49 | @unlink($file->path()); 50 | @rmdir($defaultPath); 51 | } 52 | 53 | #[TestWith(['2018_12_01_081000_create_test_index'])] 54 | #[TestWith(['2019_08_10_142230_update_test_index_mapping'])] 55 | #[TestWith([__DIR__ . '/../../migrations/archive/2017_11_11_100000_create_test_alias.php'])] 56 | public function test_file_can_be_retrieved_if_exists(string $fileName): void 57 | { 58 | /** @var MigrationFile $file */ 59 | $file = $this->migrationStorage->whereName($fileName); 60 | 61 | $this->assertSame(basename($fileName, MigrationFile::FILE_EXTENSION), $file->name()); 62 | } 63 | 64 | #[TestWith(['3030_01_01_000000_non_existing_file'])] 65 | #[TestWith(['test'])] 66 | #[TestWith([''])] 67 | #[TestWith([__DIR__ . '/../../migrations/archive/3030_01_01_000000_non_existing_file.php'])] 68 | public function test_file_can_not_be_retrieved_if_it_does_not_exist(string $fileName): void 69 | { 70 | $file = $this->migrationStorage->whereName($fileName); 71 | 72 | $this->assertNull($file); 73 | } 74 | 75 | public function test_all_files_within_migrations_directory_can_be_retrieved(): void 76 | { 77 | $files = $this->migrationStorage->all(); 78 | 79 | $this->assertSame( 80 | [ 81 | '2017_11_11_100000_create_test_alias', 82 | '2018_12_01_081000_create_test_index', 83 | '2019_08_10_142230_update_test_index_mapping', 84 | ], 85 | $files->map(static fn (MigrationFile $file) => $file->name())->toArray() 86 | ); 87 | } 88 | 89 | public function test_storage_is_ready_when_default_path_exists(): void 90 | { 91 | $this->assertTrue($this->migrationStorage->isReady()); 92 | } 93 | 94 | public function test_storage_is_not_ready_when_default_path_does_not_exist(): void 95 | { 96 | $this->config->set('elastic.migrations.storage.default_path', '/non_existing_directory'); 97 | 98 | // create a new instance to apply the new config 99 | $this->app->forgetInstance(MigrationStorage::class); 100 | $migrationStorage = resolve(MigrationStorage::class); 101 | 102 | $this->assertFalse($migrationStorage->isReady()); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tests/Integration/MigratorTest.php: -------------------------------------------------------------------------------- 1 | table = $this->config->get('elastic.migrations.database.table'); 30 | $this->output = $this->createMock(OutputStyle::class); 31 | $this->migrator = resolve(Migrator::class)->setOutput($this->output); 32 | 33 | // create fixtures 34 | DB::table($this->table)->insert([ 35 | ['migration' => '2018_12_01_081000_create_test_index', 'batch' => 1], 36 | ]); 37 | } 38 | 39 | public function test_single_migration_can_not_be_executed_if_file_does_not_exist(): void 40 | { 41 | $this->output 42 | ->expects($this->once()) 43 | ->method('writeln') 44 | ->with('Migration is not found: 3020_11_01_045023_drop_test_index'); 45 | 46 | $this->assertSame( 47 | $this->migrator, 48 | $this->migrator->migrateOne('3020_11_01_045023_drop_test_index') 49 | ); 50 | } 51 | 52 | public function test_single_migration_can_be_executed_if_file_exists(): void 53 | { 54 | Index::shouldReceive('putMapping')->once(); 55 | 56 | $this->output 57 | ->expects($this->exactly(2)) 58 | ->method('writeln') 59 | ->with( 60 | $this->callback(static fn (string $message) => in_array($message, [ 61 | 'Migrating: 2019_08_10_142230_update_test_index_mapping', 62 | 'Migrated: 2019_08_10_142230_update_test_index_mapping', 63 | ])) 64 | ); 65 | 66 | $this->assertSame( 67 | $this->migrator, 68 | $this->migrator->migrateOne('2019_08_10_142230_update_test_index_mapping') 69 | ); 70 | 71 | $this->assertDatabaseHas($this->table, [ 72 | 'migration' => '2019_08_10_142230_update_test_index_mapping', 73 | 'batch' => 2, 74 | ]); 75 | } 76 | 77 | public function test_all_migrations_can_not_be_executed_if_directory_is_empty(): void 78 | { 79 | // create a temporary empty directory and reconfigure the package to use it 80 | $tmpDirectory = $this->config->get('elastic.migrations.storage.default_path') . '/tmp'; 81 | @mkdir($tmpDirectory); 82 | $this->config->set('elastic.migrations.storage.default_path', $tmpDirectory); 83 | 84 | // create a new instance to apply the new config 85 | $this->app->forgetInstance(MigrationStorage::class); 86 | $migrator = resolve(Migrator::class)->setOutput($this->output); 87 | 88 | // check that there is nothing to migrate 89 | $this->output 90 | ->expects($this->once()) 91 | ->method('writeln') 92 | ->with('Nothing to migrate'); 93 | 94 | $this->assertSame($migrator, $migrator->migrateAll()); 95 | 96 | // remove the temporary directory 97 | @rmdir($tmpDirectory); 98 | } 99 | 100 | public function test_all_migrations_can_be_executed_if_directory_is_not_empty(): void 101 | { 102 | Index::shouldReceive('putMapping')->once(); 103 | 104 | $this->output 105 | ->expects($this->exactly(2)) 106 | ->method('writeln') 107 | ->with( 108 | $this->callback(static fn (string $message) => in_array($message, [ 109 | 'Migrating: 2019_08_10_142230_update_test_index_mapping', 110 | 'Migrated: 2019_08_10_142230_update_test_index_mapping', 111 | ])) 112 | ); 113 | 114 | $this->assertSame( 115 | $this->migrator, 116 | $this->migrator->migrateAll() 117 | ); 118 | 119 | $this->assertDatabaseHas($this->table, [ 120 | 'migration' => '2019_08_10_142230_update_test_index_mapping', 121 | 'batch' => 2, 122 | ]); 123 | } 124 | 125 | public function test_single_migration_can_not_be_rolled_back_if_file_does_not_exist(): void 126 | { 127 | $this->output 128 | ->expects($this->once()) 129 | ->method('writeln') 130 | ->with('Migration is not found: 3020_11_01_045023_drop_test_index'); 131 | 132 | $this->assertSame( 133 | $this->migrator, 134 | $this->migrator->rollbackOne('3020_11_01_045023_drop_test_index') 135 | ); 136 | } 137 | 138 | public function test_single_migration_can_not_be_rolled_back_if_file_is_not_yet_migrated(): void 139 | { 140 | $this->output 141 | ->expects($this->once()) 142 | ->method('writeln') 143 | ->with('Migration is not yet migrated: 2019_08_10_142230_update_test_index_mapping'); 144 | 145 | $this->assertSame( 146 | $this->migrator, 147 | $this->migrator->rollbackOne('2019_08_10_142230_update_test_index_mapping') 148 | ); 149 | } 150 | 151 | public function test_single_migration_can_be_rolled_back_if_file_exists_and_is_migrated(): void 152 | { 153 | Index::shouldReceive('drop')->once(); 154 | 155 | $this->output 156 | ->expects($this->exactly(2)) 157 | ->method('writeln') 158 | ->with( 159 | $this->callback(static fn (string $message) => in_array($message, [ 160 | 'Rolling back: 2018_12_01_081000_create_test_index', 161 | 'Rolled back: 2018_12_01_081000_create_test_index', 162 | ])) 163 | ); 164 | 165 | $this->assertSame( 166 | $this->migrator, 167 | $this->migrator->rollbackOne('2018_12_01_081000_create_test_index') 168 | ); 169 | 170 | $this->assertDatabaseMissing($this->table, [ 171 | 'migration' => '2018_12_01_081000_create_test_index', 172 | 'batch' => 1, 173 | ]); 174 | } 175 | 176 | public function test_last_batch_can_not_be_rolled_back_if_some_files_are_missing(): void 177 | { 178 | // imitate, that migration has already been migrated 179 | DB::table($this->table)->insert([ 180 | ['migration' => '2019_03_10_101500_create_test_index', 'batch' => 2], 181 | ]); 182 | 183 | $this->output 184 | ->expects($this->once()) 185 | ->method('writeln') 186 | ->with('Migration is not found: 2019_03_10_101500_create_test_index'); 187 | 188 | $this->assertSame( 189 | $this->migrator, 190 | $this->migrator->rollbackLastBatch() 191 | ); 192 | } 193 | 194 | public function test_last_batch_can_be_rolled_back_if_all_files_are_present(): void 195 | { 196 | // imitate, that migration has already been migrated 197 | DB::table($this->table)->insert([ 198 | ['migration' => '2019_08_10_142230_update_test_index_mapping', 'batch' => 4], 199 | ]); 200 | 201 | Index::shouldReceive('putMapping')->once(); 202 | 203 | $this->output 204 | ->expects($this->exactly(2)) 205 | ->method('writeln') 206 | ->with( 207 | $this->callback(static fn (string $message) => in_array($message, [ 208 | 'Rolling back: 2019_08_10_142230_update_test_index_mapping', 209 | 'Rolled back: 2019_08_10_142230_update_test_index_mapping', 210 | ])) 211 | ); 212 | 213 | $this->assertSame( 214 | $this->migrator, 215 | $this->migrator->rollbackLastBatch() 216 | ); 217 | 218 | $this->assertDatabaseMissing($this->table, [ 219 | 'migration' => '2019_08_10_142230_update_test_index_mapping', 220 | 'batch' => 4, 221 | ]); 222 | } 223 | 224 | public function test_all_migrations_can_not_be_rolled_back_if_some_files_are_missing(): void 225 | { 226 | // imitate, that migrations have already been migrated 227 | DB::table($this->table)->insert([ 228 | ['migration' => '2019_03_10_101500_create_test_index', 'batch' => 2], 229 | ['migration' => '2019_01_01_053550_drop_test_index', 'batch' => 2], 230 | ]); 231 | 232 | $this->output 233 | ->expects($this->once()) 234 | ->method('writeln') 235 | ->with( 236 | 'Migration is not found: 2019_03_10_101500_create_test_index,2019_01_01_053550_drop_test_index' 237 | ); 238 | 239 | $this->assertSame( 240 | $this->migrator, 241 | $this->migrator->rollbackAll() 242 | ); 243 | } 244 | 245 | public function test_all_migrations_can_be_rolled_back_if_all_files_are_present(): void 246 | { 247 | // imitate, that migration has already been migrated 248 | DB::table($this->table)->insert([ 249 | ['migration' => '2019_08_10_142230_update_test_index_mapping', 'batch' => 2], 250 | ]); 251 | 252 | Index::shouldReceive('putMapping')->once(); 253 | Index::shouldReceive('drop')->once(); 254 | 255 | $this->output 256 | ->expects($this->exactly(4)) 257 | ->method('writeln') 258 | ->with( 259 | $this->callback(static fn (string $message) => in_array($message, [ 260 | 'Rolling back: 2019_08_10_142230_update_test_index_mapping', 261 | 'Rolled back: 2019_08_10_142230_update_test_index_mapping', 262 | 'Rolling back: 2018_12_01_081000_create_test_index', 263 | 'Rolled back: 2018_12_01_081000_create_test_index', 264 | ])) 265 | ); 266 | 267 | $this->assertSame( 268 | $this->migrator, 269 | $this->migrator->rollbackAll() 270 | ); 271 | 272 | $this->assertDatabaseMissing($this->table, [ 273 | 'migration' => '2019_08_10_142230_update_test_index_mapping', 274 | 'batch' => 2, 275 | ]); 276 | 277 | $this->assertDatabaseMissing($this->table, [ 278 | 'migration' => '2018_12_01_081000_create_test_index', 279 | 'batch' => 1, 280 | ]); 281 | } 282 | 283 | public static function statusDataProvider(): array 284 | { 285 | return [ 286 | 'all migrations' => [ 287 | 'onlyPending' => false, 288 | 'expectedOutput' => [ 289 | ['2018_12_01_081000_create_test_index', 'Ran (last batch)'], 290 | ['2019_08_10_142230_update_test_index_mapping', 'Pending'], 291 | ], 292 | ], 293 | 'pending migrations' => [ 294 | 'onlyPending' => true, 295 | 'expectedOutput' => [ 296 | ['2019_08_10_142230_update_test_index_mapping', 'Pending'], 297 | ], 298 | ], 299 | ]; 300 | } 301 | 302 | #[DataProvider('statusDataProvider')] 303 | public function test_status_is_displayed_correctly(bool $onlyPending, array $expectedOutput): void 304 | { 305 | $this->output 306 | ->expects($this->once()) 307 | ->method('table') 308 | ->with(['Migration name', 'Status'], $expectedOutput); 309 | 310 | $this->assertSame( 311 | $this->migrator, 312 | $this->migrator->showStatus($onlyPending) 313 | ); 314 | } 315 | 316 | public function test_migrator_is_ready_when_repository_and_storage_are_ready(): void 317 | { 318 | $this->assertTrue($this->migrator->isReady()); 319 | } 320 | 321 | public function test_migrator_is_not_ready_when_repository_is_not_ready(): void 322 | { 323 | Schema::drop($this->table); 324 | 325 | $this->output 326 | ->expects($this->once()) 327 | ->method('writeln') 328 | ->with('Migration table is not yet created'); 329 | 330 | $this->assertFalse($this->migrator->isReady()); 331 | } 332 | 333 | public function test_migrator_is_not_ready_when_storage_is_not_ready(): void 334 | { 335 | $this->config->set('elastic.migrations.storage.default_path', '/non_existing_directory'); 336 | 337 | // create a new instance to apply the new config 338 | $this->app->forgetInstance(MigrationStorage::class); 339 | $migrator = $this->app->make(Migrator::class)->setOutput($this->output); 340 | 341 | $this->output 342 | ->expects($this->once()) 343 | ->method('writeln') 344 | ->with('Default migration path is not yet created'); 345 | 346 | $this->assertFalse($migrator->isReady()); 347 | } 348 | } 349 | -------------------------------------------------------------------------------- /tests/Integration/Repositories/MigrationRepositoryTest.php: -------------------------------------------------------------------------------- 1 | table = $this->config->get('elastic.migrations.database.table'); 25 | 26 | // create fixtures 27 | DB::table($this->table)->insert([ 28 | ['migration' => '2019_08_10_142230_update_test_index_mapping', 'batch' => 2], 29 | ['migration' => '2018_12_01_081000_create_test_index', 'batch' => 1], 30 | ]); 31 | 32 | $this->migrationRepository = new MigrationRepository(); 33 | } 34 | 35 | public function test_record_can_be_inserted(): void 36 | { 37 | $this->migrationRepository->insert('2019_12_12_201657_update_test_index_settings', 3); 38 | 39 | $this->assertDatabaseHas( 40 | $this->table, 41 | ['migration' => '2019_12_12_201657_update_test_index_settings', 'batch' => 3] 42 | ); 43 | } 44 | 45 | public function test_record_passes_existence_check(): void 46 | { 47 | $this->assertTrue($this->migrationRepository->exists('2018_12_01_081000_create_test_index')); 48 | $this->assertFalse($this->migrationRepository->exists('2019_12_05_092345_drop_test_index')); 49 | } 50 | 51 | public function test_record_can_be_deleted(): void 52 | { 53 | $this->migrationRepository->delete('2019_12_01_081000_create_test_index'); 54 | 55 | $this->assertDatabaseMissing( 56 | $this->table, 57 | ['migration' => '2019_12_01_081000_create_test_index', 'batch' => 1] 58 | ); 59 | } 60 | 61 | public function test_all_records_can_be_retrieved(): void 62 | { 63 | $this->assertSame( 64 | $this->migrationRepository->all()->toArray(), 65 | [ 66 | '2019_08_10_142230_update_test_index_mapping', 67 | '2018_12_01_081000_create_test_index', 68 | ] 69 | ); 70 | } 71 | 72 | public function test_last_batch_number_can_be_retrieved(): void 73 | { 74 | $this->assertSame(2, $this->migrationRepository->lastBatchNumber()); 75 | 76 | DB::table($this->table)->delete(); 77 | $this->assertNull($this->migrationRepository->lastBatchNumber()); 78 | } 79 | 80 | public function test_last_record_batch_can_be_retrieved(): void 81 | { 82 | $this->assertSame( 83 | $this->migrationRepository->lastBatch()->toArray(), 84 | [ 85 | '2019_08_10_142230_update_test_index_mapping', 86 | ] 87 | ); 88 | } 89 | 90 | public function test_repository_is_ready_when_table_exists(): void 91 | { 92 | $this->assertTrue($this->migrationRepository->isReady()); 93 | } 94 | 95 | public function test_repository_is_not_ready_when_table_does_not_exist(): void 96 | { 97 | Schema::drop($this->table); 98 | 99 | $this->assertFalse($this->migrationRepository->isReady()); 100 | } 101 | 102 | public function test_repository_can_delete_all_records(): void 103 | { 104 | $this->assertCount(2, $this->migrationRepository->all()); 105 | 106 | $this->migrationRepository->purge(); 107 | 108 | $this->assertCount(0, $this->migrationRepository->all()); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /tests/Integration/TestCase.php: -------------------------------------------------------------------------------- 1 | config = $app['config']; 30 | $this->config->set('elastic.migrations.database.table', 'test_elastic_migrations'); 31 | $this->config->set('elastic.migrations.storage.default_path', realpath(__DIR__ . '/../migrations')); 32 | 33 | $app->singleton(Client::class, function () { 34 | $httpClientMock = $this->createMock(ClientInterface::class); 35 | 36 | return ClientBuilder::create() 37 | ->setHttpClient($httpClientMock) 38 | ->build(); 39 | }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/Unit/Filesystem/MigrationFileTest.php: -------------------------------------------------------------------------------- 1 | assertSame( 17 | self::FULL_PATH, 18 | (new MigrationFile(self::FULL_PATH))->path() 19 | ); 20 | } 21 | 22 | public function test_name_getter(): void 23 | { 24 | $this->assertSame( 25 | basename(self::FULL_PATH, MigrationFile::FILE_EXTENSION), 26 | (new MigrationFile(self::FULL_PATH))->name() 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/migrations/2018_12_01_081000_create_test_index.php: -------------------------------------------------------------------------------- 1 | client = $client; 14 | } 15 | 16 | public function up(): void 17 | { 18 | Index::create('test'); 19 | 20 | $this->client->indices()->clearCache([ 21 | 'index' => 'test', 22 | ]); 23 | } 24 | 25 | public function down(): void 26 | { 27 | Index::drop('test'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/migrations/2019_08_10_142230_update_test_index_mapping.php: -------------------------------------------------------------------------------- 1 | enableSource(); 13 | $mapping->text('title'); 14 | }); 15 | } 16 | 17 | public function down(): void 18 | { 19 | Index::putMapping('test', static function (Mapping $mapping) { 20 | $mapping->disableSource(); 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/migrations/archive/2017_11_11_100000_create_test_alias.php: -------------------------------------------------------------------------------- 1 |