├── .github └── workflows │ └── tests.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── composer.json └── src ├── Console ├── MigrateMakeCommand.php └── ModelMakeCommand.php ├── SqlMigration.php └── SqlMigrationsServiceProvider.php /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | tests: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: true 14 | matrix: 15 | php: [7.2, 7.3, 7.4] 16 | laravel: [^5.5, ^6.0, ^7.0, ^8.0] 17 | exclude: 18 | - php: 7.2 19 | laravel: ^8.0 20 | include: 21 | - php: 8.0.2 22 | laravel: ^8.0 23 | - php: 8.1 24 | laravel: ^9.0 25 | name: Test PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} 26 | steps: 27 | - name: Checkout code 28 | uses: actions/checkout@v2 29 | 30 | - name: Setup PHP 31 | uses: shivammathur/setup-php@v2 32 | with: 33 | php-version: ${{ matrix.php }} 34 | extensions: dom, curl, libxml, mbstring, zip 35 | tools: composer:v2 36 | coverage: xdebug 37 | 38 | - name: Install dependencies 39 | run: | 40 | composer require "laravel/framework=${{ matrix.laravel }}" --no-update 41 | composer update --prefer-dist --no-interaction --no-progress 42 | - name: Run tests 43 | run: vendor/bin/phpunit --verbose --coverage-clover coverage.xml 44 | 45 | - name: Upload code coverage 46 | if: matrix.php == '8.0.2' && matrix.laravel == '^9.0' 47 | uses: codecov/codecov-action@v1 48 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.2.0](https://github.com/pmatseykanets/laravel-sql-migrations/releases/tag/v1.2.0) 2022-05-06 4 | 5 | ## Changed 6 | 7 | - Added Laravel 9 support 8 | 9 | ## [1.1.0](https://github.com/pmatseykanets/laravel-sql-migrations/releases/tag/v1.1.0) 2021-01-06 10 | 11 | ### Changed 12 | 13 | - Added PHP 8 support 14 | 15 | ## [1.0.0](https://github.com/pmatseykanets/laravel-sql-migrations/releases/tag/v1.0.0) 2020-11-25 16 | 17 | ### Changed 18 | 19 | - Added Laravel 7 and 8 support 20 | 21 | ## [0.4.0](https://github.com/pmatseykanets/laravel-sql-migrations/releases/tag/v0.4.0) 2019-10-25 22 | 23 | ### Changed 24 | 25 | - Added Laravel 6 support 26 | 27 | ## [0.3.0](https://github.com/pmatseykanets/laravel-sql-migrations/releases/tag/v0.3.0) 2018-10-29 28 | 29 | ### Changed 30 | 31 | - Changed the constraint for `laravel/framework` to allow future versions 32 | 33 | ## [0.2.0](https://github.com/pmatseykanets/laravel-sql-migrations/releases/tag/v0.2.0) 2018-06-26 34 | 35 | ### Added 36 | 37 | - Extended `MakeModelCommand` to allow creating plain SQL migrations when creating a model 38 | 39 | ## [0.1.0](https://github.com/pmatseykanets/laravel-sql-migrations/releases/tag/v0.1.0) 2018-06-18 40 | 41 | Initial experimental release 42 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/spatie/laravel-backup). 6 | 7 | 8 | ## Pull Requests 9 | 10 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP CS Fixer](https://cs.sensiolabs.org). 11 | 12 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 13 | 14 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 15 | 16 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). 17 | 18 | - **Create feature branches** - Don't ask us to pull from your master branch. 19 | 20 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 21 | 22 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 23 | 24 | 25 | ## Running Tests 26 | 27 | ```bash 28 | composer test 29 | ``` 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) Peter Matseykanets 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 13 | > all 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 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # laravel-sql-migrations 2 | 3 | ![tests](https://github.com/pmatseykanets/laravel-sql-migrations/workflows/tests/badge.svg) 4 | [![Total Downloads](https://img.shields.io/packagist/dt/pmatseykanets/laravel-sql-migrations.svg?style=flat-square)](https://packagist.org/packages/pmatseykanets/laravel-sql-migrations) 5 | 6 | 7 | Write your Laravel migrations in plain SQL. 8 | 9 | If you find this package usefull, please consider bying me a coffee. 10 | 11 | Buy Me a Coffee at ko-fi.com 12 | 13 | ## Contents 14 | 15 | - [Why](#why) 16 | - [Installation](#installation) 17 | - [Usage](#usage) 18 | - [Make SQL Migrations](#make-sql-migrations) 19 | - [Run SQL Migrations](#run-sql-migrations) 20 | - [Example Projects](#example-projects) 21 | - [Changelog](#changelog) 22 | - [Contributing](#contributing) 23 | - [Credits](#credits) 24 | - [License](#license) 25 | 26 | ## Why 27 | 28 | Don't get me wrong, the Laravel's [`SchemaBuilder`](https://laravel.com/docs/master/migrations) is absolutely great and you can get a lot of millage out of it. 29 | 30 | But there are cases when it's just standing in the way. Below are just a few examples where `SchemaBuilder` falls short. 31 | 32 | ### Using additional / richer data types 33 | 34 | I.e. if you're using [PostgreSQL](https://www.postgresql.org/) and you want to use a case insensitive data type for string/text data you may consider `CITEXT`. This means that we have to resort to a hack like this 35 | 36 | ```php 37 | class CreateUsersTable extends Migration 38 | { 39 | public function up() 40 | { 41 | Schema::create('users', function (Blueprint $table) { 42 | $table->bigIncrement('id'); 43 | $table->string('email')->unique(); 44 | // ... 45 | }); 46 | 47 | DB::unprepared('ALTER TABLE users ALTER COLUMN email TYPE CITEXT'); 48 | } 49 | } 50 | ``` 51 | 52 | instead of just 53 | 54 | ```sql 55 | CREATE TABLE IF NOT EXISTS users ( 56 | id BIGSERIAL PRIMARY KEY, 57 | email CITEXT UNIQUE, 58 | ... 59 | ); 60 | ``` 61 | 62 | Of course there are plenty of other data types (i.e. [Range](https://www.postgresql.org/docs/current/static/rangetypes.html) or [Text Search](https://www.postgresql.org/docs/current/static/datatype-textsearch.html) data types in PostgreSQL) that might be very useful but `SchemaBuilder` is unaware of and never will be. 63 | 64 | ### Managing stored functions, procedures and triggers 65 | 66 | This is a big one, especially if you're still using reverse (`down()`) migrations. This means that you need to cram both new and old source code of a function/procedure/trigger in `up()` and `down()` methods of your migration file and keep them in string variables which doesn't help with readability/maintainability. 67 | 68 | Even with [`heredoc` / `nowdoc`](https://secure.php.net/manual/en/language.types.string.php) syntax in `php` it's still gross. 69 | 70 | ### Taking advantage of `IF [NOT] EXISTS` and alike 71 | 72 | There is a multitude of important and useful SQL standard compliant and vendor specific clauses in DDL statements that can make your life so much easier. One of the well known and frequently used ones is `IF [NOT] EXISTS`. 73 | 74 | Instead of letting `ShemaBuilder` doing a separate query(ies) to `information_schema` 75 | 76 | ```php 77 | if (! Schema::hasTable('users')) { 78 | // create the table 79 | } 80 | 81 | if (! Schema::hasColumn('users', 'notes')) { 82 | // create the column 83 | } 84 | ``` 85 | 86 | you can just write it natively in one statement 87 | 88 | ```sql 89 | CREATE TABLE IF NOT EXISTS users (id BIGSERIAL PRIMARY KEY, ...); 90 | ALTER TABLE users ADD IF NOT EXISTS notes TEXT; 91 | ``` 92 | 93 | ### Using additional options when creating indexes 94 | 95 | Some databases (i.e. PostgreSQL) allow you to (re)create indexes concurrently without locking your table. 96 | 97 | ```sql 98 | CREATE INDEX CONCURRENTLY IF NOT EXISTS some_big_table_important_column_id 99 | ON some_big_table (important_column); 100 | 101 | CREATE INDEX IF NOT EXISTS table_json_column_idx USING GIN ON table (json_column); 102 | ``` 103 | 104 | You may need to create a specific type of index instead of a default `btree` 105 | 106 | ```sql 107 | CREATE INDEX IF NOT EXISTS some_table_json_column_idx ON some_table (json_column) USING GIN; 108 | ``` 109 | 110 | Or create a partial/functional index 111 | 112 | ```sql 113 | CREATE INDEX IF NOT EXISTS some_table_nullable_column_idx 114 | ON some_table (nullable_column) 115 | WHERE nullable_column IS NOT NULL; 116 | ``` 117 | 118 | ### Taking advantage of database native procedural code (i.e. PL/pgSQL) 119 | 120 | When using PostgreSQL you can use an anonymous [PL/pgSQL](https://www.postgresql.org/docs/current/static/plpgsql.html) code block if you need to. I.e. dynamically (without knowing the database name ahead of time) set `search_path` if you want to install all extensions in a dedicated schema instead of polluting `public`. 121 | 122 | The `.up.sql` migration could look like: 123 | 124 | ```sql 125 | DO $$ 126 | BEGIN 127 | EXECUTE 'ALTER DATABASE ' || current_database() || ' SET search_path TO "$user",public,extensions'; 128 | END; 129 | $$; 130 | ``` 131 | 132 | and the reverse `.down.sql`: 133 | 134 | ```sql 135 | DO $$ 136 | BEGIN 137 | EXECUTE 'ALTER DATABASE ' || current_database() || ' SET search_path TO "$user",public'; 138 | END; 139 | $$; 140 | ``` 141 | 142 | ## Installation 143 | 144 | You can install the package via composer: 145 | 146 | ```bash 147 | composer require pmatseykanets/laravel-sql-migrations 148 | ``` 149 | 150 | If you're using Laravel < 5.5 or if you have package auto-discovery turned off you have to manually register the service provider: 151 | 152 | ```php 153 | // config/app.php 154 | 'providers' => [ 155 | ... 156 | SqlMigrations\SqlMigrationsServiceProvider::class, 157 | ], 158 | ``` 159 | 160 | ## Usage 161 | 162 | ### Make SQL migrations 163 | 164 | The most convenient way of creating SQL migrations is to use `artisan make:migration` with **`--sql`** option 165 | 166 | ```bash 167 | php artisan make:migration create_users_table --sql 168 | ``` 169 | 170 | which will produce three files 171 | 172 | ```bash 173 | database 174 | └── migrations 175 | ├── 2018_06_15_000000_create_users_table.down.sql 176 | ├── 2018_06_15_000000_create_users_table.php 177 | └── 2018_06_15_000000_create_users_table.up.sql 178 | ``` 179 | 180 | *I know, it bloats `migrations` directory with additional files but this approach allows you to mix and match traditional and plain SQL migrations easily. If it's any consolation if you don't use reverse (`down`) migrations you can just delete `*.down.sql` file(s).* 181 | 182 | **Note:** if you're creating files manually make sure that: 183 | 184 | 1. The base `php` migration class extends `SqlMigration` class and doesn't contain `up()` and `down()` methods, unless you mean to override the default behavior. 185 | 2. The filename (without extension) of `.up.sql` and `.down.sql` files matches exactly (including the timestamp part) the filename of the base `php` migration. 186 | 187 | At this point you can forget about `2018_06_15_000000_create_users_table.php` unless you want to configure or override behavior of this particular migration. 188 | 189 | `SqlMigration` extends the built-in `Migration` so you can fine tune your migration in the same way 190 | 191 | ```php 192 | class CreateNextIdFunction extends SqlMigration 193 | { 194 | // Use a non default connection 195 | public $connection = 'pgsql2'; 196 | // Wrap migration in a transaction if the database suports transactional DDL 197 | public $withinTransaction = true; 198 | } 199 | ``` 200 | 201 | Now go ahead open up `*.sql` files and write your migration code. 202 | 203 | I.e. `2018_06_15_000000_create_users_table.up.sql` might look along the lines of 204 | 205 | ```sql 206 | CREATE TABLE IF NOT EXISTS users ( 207 | id BIGSERIAL PRIMARY KEY, 208 | name CITEXT, 209 | email CITEXT, 210 | password TEXT, 211 | remember_token TEXT, 212 | created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, 213 | updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP 214 | ); 215 | 216 | CREATE UNIQUE INDEX IF NOT EXISTS users_email_idx ON users (email); 217 | ``` 218 | 219 | and `2018_06_15_000000_create_users_table.down.sql` 220 | 221 | ```sql 222 | DROP TABLE IF EXISTS users; 223 | ``` 224 | 225 | You can also pass `--sql` option to `make:model` artisan command to instruct it to create plain SQL migrations for your newly created model. 226 | 227 | ```bash 228 | php artisan make:model Post --migration --sql 229 | ``` 230 | 231 | ### Run SQL migrations 232 | 233 | Proceed as usual using `migrate`, `migrate:rollback` and other built-in commands. 234 | 235 | ## Example Projects 236 | 237 | You can find bare Laravel 5.6 projects with default SQL migrations here: 238 | 239 | - [PostgreSQL](https://github.com/pmatseykanets/laravel-sql-migrations-example-postgres) 240 | - [MySQL](https://github.com/pmatseykanets/laravel-sql-migrations-example-mysql) 241 | 242 | ## Changelog 243 | 244 | Please see [CHANGELOG](CHANGELOG.md) for more information about what has changed recently. 245 | 246 | ## Contributing 247 | 248 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 249 | 250 | ## Credits 251 | 252 | - [Peter Matseykanets](https://github.com/pmatseykanets) 253 | - [All Contributors](../../contributors) 254 | 255 | ## License 256 | 257 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 258 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pmatseykanets/laravel-sql-migrations", 3 | "description": "Raw SQL migrations for Laravel", 4 | "type": "library", 5 | "license": "MIT", 6 | "homepage": "https://github.com/pmatseykanets/laravel-sql-migrations", 7 | "authors": [ 8 | { 9 | "name": "Peter Matseykanets", 10 | "email": "pmatseykanets@gmail.com" 11 | } 12 | ], 13 | "require": { 14 | "php": "^7.2|^8.0.2", 15 | "laravel/framework": "~5.5|~6.0|~7.0|~8.0|~9.0" 16 | }, 17 | "require-dev": { 18 | "mockery/mockery": "^1.2.3", 19 | "phpunit/phpunit": "^8.3" 20 | }, 21 | "autoload": { 22 | "psr-4": { 23 | "SqlMigrations\\": "src" 24 | } 25 | }, 26 | "autoload-dev": { 27 | "classmap": [ 28 | "tests/database/migrations" 29 | ], 30 | "psr-4": { 31 | "Tests\\": "tests" 32 | } 33 | }, 34 | "scripts": { 35 | "test": "vendor/bin/phpunit" 36 | }, 37 | "extra": { 38 | "laravel": { 39 | "providers": [ 40 | "SqlMigrations\\SqlMigrationsServiceProvider" 41 | ] 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Console/MigrateMakeCommand.php: -------------------------------------------------------------------------------- 1 | signature .= '{--sql : Create a plain SQL migration}'; 26 | 27 | parent::__construct($creator, $composer); 28 | } 29 | 30 | /** 31 | * Write the migration file to disk. 32 | * 33 | * @param string $name 34 | * @param string $table 35 | * @param bool $create 36 | */ 37 | protected function writeMigration($name, $table, $create) 38 | { 39 | if (! $this->option('sql')) { 40 | return parent::writeMigration($name, $table, $create); 41 | } 42 | 43 | $path = $this->creator->create($name, $this->getMigrationPath(), $table, $create); 44 | 45 | $this->replaceMigrationContent($path, $name); 46 | $this->createSqlMigrationStubs($path); 47 | 48 | $file = pathinfo($path, PATHINFO_FILENAME); 49 | 50 | $this->line("Created Migration: {$file}"); 51 | } 52 | 53 | protected function replaceMigrationContent($path, $name) 54 | { 55 | $className = Str::studly($name); 56 | 57 | $stub = str_replace('DummyClass', $className, $this->stub); 58 | 59 | file_put_contents($path, $stub); 60 | } 61 | 62 | protected function createSqlMigrationStubs($path) 63 | { 64 | $basePath = preg_replace('/\.php$/', '', $path); 65 | 66 | file_put_contents("$basePath.up.sql", "-- Run the migrations\n"); 67 | file_put_contents("$basePath.down.sql", "-- Reverse the migrations\n"); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Console/ModelMakeCommand.php: -------------------------------------------------------------------------------- 1 | argument('name')))); 19 | 20 | $with = [ 21 | 'name' => "create_{$table}_table", 22 | '--create' => $table, 23 | ]; 24 | 25 | if ($this->option('sql')) { 26 | $with['--sql'] = true; 27 | } 28 | 29 | $this->call('make:migration', $with); 30 | } 31 | 32 | /** 33 | * Get the console command options. 34 | * 35 | * @return array 36 | */ 37 | protected function getOptions() 38 | { 39 | $options = parent::getOptions(); 40 | $options[] = ['sql', 'M', InputOption::VALUE_NONE, 'Create a plain SQL migration.']; 41 | 42 | return $options; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/SqlMigration.php: -------------------------------------------------------------------------------- 1 | apply('up'); 18 | } 19 | 20 | /** 21 | * Reverse the migrations. 22 | * 23 | * @return void 24 | */ 25 | public function down() 26 | { 27 | $this->apply('down'); 28 | } 29 | 30 | /** 31 | * Apply the migration. 32 | * 33 | * @param $path 34 | * @param mixed $direction 35 | */ 36 | public function apply($direction) 37 | { 38 | if (file_exists($path = $this->migrationFile($direction))) { 39 | DB::connection($this->getConnection())->unprepared(file_get_contents($path)); 40 | } 41 | } 42 | 43 | public function migrationFile($direction) 44 | { 45 | return preg_replace( 46 | '/\.php$/', 47 | ".$direction.sql", 48 | (new \ReflectionObject($this))->getFileName() 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/SqlMigrationsServiceProvider.php: -------------------------------------------------------------------------------- 1 | extendMigrateMakeCommand(); 19 | } 20 | 21 | /** 22 | * Extend MigrateMake command. 23 | * 24 | * @return void 25 | */ 26 | protected function extendMigrateMakeCommand() 27 | { 28 | $migrateMakeAbstract = 'Illuminate\Database\Console\Migrations\MigrateMakeCommand'; 29 | $modelMakeAbstract = 'Illuminate\Foundation\Console\ModelMakeCommand'; 30 | 31 | $appMajor = explode('.', $this->app->version())[0]; 32 | if ($appMajor < 9) { 33 | $migrateMakeAbstract = 'command.migrate.make'; 34 | $modelMakeAbstract = 'command.model.make'; 35 | } 36 | 37 | $this->app->extend($migrateMakeAbstract, function ($command, $app) { 38 | return new MigrateMakeCommand( 39 | $app['migration.creator'], 40 | $app['composer'] 41 | ); 42 | }); 43 | 44 | $this->app->extend($modelMakeAbstract, function ($command, $app) { 45 | return new ModelMakeCommand( 46 | $app['files'] 47 | ); 48 | }); 49 | } 50 | } 51 | --------------------------------------------------------------------------------