diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8658d0f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +/tests export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.scrutinizer.yml export-ignore +.travis.yml export-ignore +phpunit.xml.dist export-ignore \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..431fa42 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/.idea +/.vagrant +/vendor +after.sh +aliases +composer.lock +Homestead.yaml +Vagrantfile \ No newline at end of file diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 0000000..5452de5 --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,4 @@ +tools: + external_code_coverage: + runs: 3 + timeout: 600 \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..be22f90 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,55 @@ +language: php + +dist: xenial + +services: + - postgresql + +addons: + mariadb: 10.2 + +env: + global: + - COVERAGE=no + - DB=mysql + - RELEASE=stable + +matrix: + include: + - php: 7.0 + - php: 7.0 + env: RELEASE=lowest + - php: 7.1 + - php: 7.2 + - php: 7.3 + env: COVERAGE=yes + - php: 7.3 + env: COVERAGE=yes DB=pgsql + - php: 7.3 + env: COVERAGE=yes DB=sqlite + +cache: + directories: + - $HOME/.composer/cache + +before_install: + - COMPOSER_FLAGS=$([ $RELEASE == "lowest" ] && echo "--prefer-lowest" || echo "") + - PHPUNIT_FLAGS=$([ $COVERAGE == "yes" ] && echo "--coverage-clover=coverage.xml" || echo "") + +install: + - travis_retry composer update --no-interaction --no-suggest --prefer-dist --prefer-stable $COMPOSER_FLAGS + +before_script: + - cp tests/config/database.travis.php tests/config/database.php + - mysql -e 'create database `test`;' + - psql -c 'create database "test";' -U postgres + +script: + - vendor/bin/phpunit $PHPUNIT_FLAGS + +after_script: + - | + if [ $COVERAGE == "yes" ]; then + travis_retry wget https://scrutinizer-ci.com/ocular.phar + travis_retry php ocular.phar code-coverage:upload --format=php-clover coverage.xml + fi \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..870c0d9 --- /dev/null +++ b/README.md @@ -0,0 +1,106 @@ +[![Build Status](https://travis-ci.org/staudenmeir/laravel-cte.svg?branch=master)](https://travis-ci.org/staudenmeir/laravel-cte) +[![Code Coverage](https://scrutinizer-ci.com/g/staudenmeir/laravel-cte/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/staudenmeir/laravel-cte/?branch=master) +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/staudenmeir/laravel-cte/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/staudenmeir/laravel-cte/?branch=master) +[![Latest Stable Version](https://poser.pugx.org/staudenmeir/laravel-cte/v/stable)](https://packagist.org/packages/staudenmeir/laravel-cte) +[![Total Downloads](https://poser.pugx.org/staudenmeir/laravel-cte/downloads)](https://packagist.org/packages/staudenmeir/laravel-cte) +[![License](https://poser.pugx.org/staudenmeir/laravel-cte/license)](https://packagist.org/packages/staudenmeir/laravel-cte) + +## Introduction +This Laravel extension adds support for common table expressions (CTE) to the query builder and Eloquent. +Supports Laravel 5.5+. + +## Installation + + composer require staudenmeir/laravel-cte:"^1.0" + +## Usage + +- [SELECT Queries](#select-queries) +- [INSERT/UPDATE/DELETE Queries](#insertupdatedelete-queries) +- [Eloquent](#eloquent) + +### SELECT Queries + +Use `withExpression()` and provide a query builder instance, an SQL string or a closure: + +```php +$posts = DB::table('p') + ->select('p.*', 'u.name') + ->withExpression('p', DB::table('posts')) + ->withExpression('u', function ($query) { + $query->from('users'); + }) + ->join('u', 'u.id', '=', 'p.user_id') + ->get(); +``` + +Use `withRecursiveExpression()` for recursive expressions: + +```php +$query = DB::table('users') + ->whereNull('parent_id') + ->unionAll( + DB::table('users') + ->select('users.*') + ->join('tree', 'tree.id', '=', 'users.parent_id') + ); + +$tree = DB::table('tree') + ->withRecursiveExpression('tree', $query) + ->get(); +``` + +You can provide the expression's columns as the third argument: + +```php +$query = 'select 1 union all select number + 1 from numbers where number < 10'; + +$numbers = DB::table('numbers') + ->withRecursiveExpression('numbers', $query, ['number']) + ->get(); +``` + +### INSERT/UPDATE/DELETE Queries + +You can use common table expressions in `INSERT`(Laravel 5.7.17+), `UPDATE` and `DELETE` queries: + +```php +DB::table('profiles') + ->withExpression('u', DB::table('users')->select('id', 'name')) + ->insertUsing(['user_id', 'name'], DB::table('u')); +``` + +```php +DB::table('profiles') + ->withExpression('u', DB::table('users')) + ->join('u', 'u.id', '=', 'profiles.user_id') + ->update(['profiles.name' => DB::raw('u.name')]); +``` + +```php +DB::table('profiles') + ->withExpression('u', DB::table('users')->where('active', false)) + ->whereIn('user_id', DB::table('u')->select('id')) + ->delete(); +``` + +### Eloquent + +You can use common table expressions in Eloquent queries with the `QueriesExpressions` trait: + +```php +class User extends Model +{ + use \Staudenmeir\LaravelCte\Eloquent\QueriesExpressions; +} + +$query = User::whereNull('parent_id') + ->unionAll( + User::select('users.*') + ->join('tree', 'tree.id', '=', 'users.parent_id') + ); + +$tree = User::from('tree') + ->withRecursiveExpression('tree', $query) + ->get(); +``` \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..c0e1f61 --- /dev/null +++ b/composer.json @@ -0,0 +1,36 @@ +{ + "name": "staudenmeir/laravel-cte", + "description": "Laravel queries with common table expressions", + "license": "MIT", + "authors": [ + { + "name": "Jonas Staudenmeir", + "email": "mail@jonas-staudenmeir.de" + } + ], + "require": { + "php": ">=7.0", + "illuminate/database": "5.5.*|5.6.*|5.7.*" + }, + "require-dev": { + "laravel/homestead": "^7.0", + "orchestra/testbench": "^3.5" + }, + "autoload": { + "psr-4": { + "Staudenmeir\\LaravelCte\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/" + } + }, + "extra": { + "laravel": { + "providers": [ + "Staudenmeir\\LaravelCte\\DatabaseServiceProvider" + ] + } + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..0031bb0 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,23 @@ + + + + + ./tests + + + + + ./src + + + \ No newline at end of file diff --git a/src/Connections/CreatesQueryBuilder.php b/src/Connections/CreatesQueryBuilder.php new file mode 100644 index 0000000..8d70536 --- /dev/null +++ b/src/Connections/CreatesQueryBuilder.php @@ -0,0 +1,18 @@ +getQueryGrammar(), $this->getPostProcessor()); + } +} diff --git a/src/Connections/MySqlConnection.php b/src/Connections/MySqlConnection.php new file mode 100644 index 0000000..5ac8279 --- /dev/null +++ b/src/Connections/MySqlConnection.php @@ -0,0 +1,21 @@ +withTablePrefix(new MySqlGrammar); + } +} diff --git a/src/Connections/PostgresConnection.php b/src/Connections/PostgresConnection.php new file mode 100644 index 0000000..1c6b769 --- /dev/null +++ b/src/Connections/PostgresConnection.php @@ -0,0 +1,21 @@ +withTablePrefix(new PostgresGrammar); + } +} diff --git a/src/Connections/SQLiteConnection.php b/src/Connections/SQLiteConnection.php new file mode 100644 index 0000000..4af72c5 --- /dev/null +++ b/src/Connections/SQLiteConnection.php @@ -0,0 +1,21 @@ +withTablePrefix(new SQLiteGrammar); + } +} diff --git a/src/Connections/SqlServerConnection.php b/src/Connections/SqlServerConnection.php new file mode 100644 index 0000000..c0b8e96 --- /dev/null +++ b/src/Connections/SqlServerConnection.php @@ -0,0 +1,21 @@ +withTablePrefix(new SqlServerGrammar); + } +} diff --git a/src/Connectors/ConnectionFactory.php b/src/Connectors/ConnectionFactory.php new file mode 100644 index 0000000..f88ee73 --- /dev/null +++ b/src/Connectors/ConnectionFactory.php @@ -0,0 +1,46 @@ +app->singleton('db.factory', function ($app) { + return new ConnectionFactory($app); + }); + } +} diff --git a/src/Eloquent/QueriesExpressions.php b/src/Eloquent/QueriesExpressions.php new file mode 100644 index 0000000..6146204 --- /dev/null +++ b/src/Eloquent/QueriesExpressions.php @@ -0,0 +1,16 @@ +getConnection()->query(); + } +} diff --git a/src/Grammars/CompilesExpressions.php b/src/Grammars/CompilesExpressions.php new file mode 100644 index 0000000..075cd38 --- /dev/null +++ b/src/Grammars/CompilesExpressions.php @@ -0,0 +1,88 @@ +selectComponents, 'expressions'); + } + + /** + * Compile the common table expressions. + * + * @param \Illuminate\Database\Query\Builder $query + * @return string + */ + public function compileExpressions(Builder $query) + { + if (! $query->expressions) { + return ''; + } + + $recursive = $this->recursiveKeyword($query->expressions); + + $statements = []; + + foreach ($query->expressions as $expression) { + $columns = $expression['columns'] ? '('.$this->columnize($expression['columns']).') ' : ''; + + $statements[] = $this->wrap($expression['name']).' '.$columns.'as ('.$expression['query'].')'; + } + + return 'with '.$recursive.implode($statements, ', '); + } + + /** + * Get the "recursive" keyword. + * + * @param array $expressions + * @return string + */ + protected function recursiveKeyword(array $expressions) + { + return collect($expressions)->where('recursive', true)->isNotEmpty() ? 'recursive ' : ''; + } + + /** + * Compile an insert statement using a subquery into SQL. + * + * @param \Illuminate\Database\Query\Builder $query + * @param array $columns + * @param string $sql + * @return string + */ + public function compileInsertUsing(Builder $query, array $columns, string $sql) + { + return $this->compileExpressions($query).' '.parent::compileInsertUsing($query, $columns, $sql); + } + + /** + * Compile an update statement into SQL. + * + * @param \Illuminate\Database\Query\Builder $query + * @param array $values + * @return string + */ + public function compileUpdate(Builder $query, $values) + { + return $this->compileExpressions($query).' '.parent::compileUpdate($query, $values); + } + + /** + * Compile a delete statement into SQL. + * + * @param \Illuminate\Database\Query\Builder $query + * @return string + */ + public function compileDelete(Builder $query) + { + return $this->compileExpressions($query).' '.parent::compileDelete($query); + } +} diff --git a/src/Grammars/MySqlGrammar.php b/src/Grammars/MySqlGrammar.php new file mode 100644 index 0000000..16feb1f --- /dev/null +++ b/src/Grammars/MySqlGrammar.php @@ -0,0 +1,26 @@ +wrapTable($query->from)} ({$this->columnize($columns)}) "; + + return $insert.$this->compileExpressions($query).' '.$sql; + } +} diff --git a/src/Grammars/PostgresGrammar.php b/src/Grammars/PostgresGrammar.php new file mode 100644 index 0000000..40abdb4 --- /dev/null +++ b/src/Grammars/PostgresGrammar.php @@ -0,0 +1,10 @@ +bindings = ['expressions' => []] + $this->bindings; + } + + /** + * Add a common table expression to the query. + * + * @param string $name + * @param \Closure|\Illuminate\Database\Query\Builder|string $query + * @param array|null $columns + * @param bool $recursive + * @return $this + */ + public function withExpression($name, $query, array $columns = null, $recursive = false) + { + list($query, $bindings) = $this->createSub($query); + + $this->expressions[] = compact('name', 'query', 'columns', 'recursive'); + + $this->addBinding($bindings, 'expressions'); + + return $this; + } + + /** + * Add a recursive common table expression to the query. + * + * @param string $name + * @param \Closure|\Illuminate\Database\Query\Builder|string $query + * @param array|null $columns + * @return $this + */ + public function withRecursiveExpression($name, $query, $columns = null) + { + return $this->withExpression($name, $query, $columns, true); + } + + /** + * Insert new records into the table using a subquery. + * + * @param array $columns + * @param \Closure|\Illuminate\Database\Query\Builder|string $query + * @return bool + */ + public function insertUsing(array $columns, $query) + { + list($sql, $bindings) = $this->createSub($query); + + $bindings = array_merge($this->bindings['expressions'], $bindings); + + return $this->connection->insert( + $this->grammar->compileInsertUsing($this, $columns, $sql), + $this->cleanBindings($bindings) + ); + } + + /** + * Create a subquery and parse it. + * + * @param \Closure|\Illuminate\Database\Query\Builder|string $query + * @return array + */ + protected function createSub($query) + { + if ($query instanceof Closure) { + $callback = $query; + + $callback($query = $this->forSubQuery()); + } + + return $this->parseSub($query); + } + + /** + * Create a new query instance for a sub-query. + * + * @return \Illuminate\Database\Query\Builder + */ + protected function forSubQuery() + { + return $this->newQuery(); + } + + /** + * Parse the subquery into SQL and bindings. + * + * @param mixed $query + * @return array + */ + protected function parseSub($query) + { + if ($query instanceof self || $query instanceof EloquentBuilder) { + return [$query->toSql(), $query->getBindings()]; + } elseif (is_string($query)) { + return [$query, []]; + } else { + throw new InvalidArgumentException; // @codeCoverageIgnore + } + } +} diff --git a/tests/EloquentTest.php b/tests/EloquentTest.php new file mode 100644 index 0000000..eb22928 --- /dev/null +++ b/tests/EloquentTest.php @@ -0,0 +1,30 @@ +whereIn('id', function (Builder $query) { + $query->from('ids'); + })->get(); + + $this->assertEquals([1, 2], $users->pluck('id')->all()); + } + + public function testWithRecursiveExpression() + { + $query = 'select * from users where id = 3 union all select users.* from users inner join parents on parents.parent_id = users.id'; + + $users = User::from('parents') + ->withRecursiveExpression('parents', $query) + ->get(); + + $this->assertEquals([3, 2, 1], $users->pluck('id')->all()); + } +} diff --git a/tests/Models/User.php b/tests/Models/User.php new file mode 100644 index 0000000..a1bcb32 --- /dev/null +++ b/tests/Models/User.php @@ -0,0 +1,11 @@ +select('u.id') + ->withExpression('u', DB::table('users')) + ->withExpression('p', function ($query) { + $query->from('posts'); + }) + ->join('p', 'p.user_id', '=', 'u.id') + ->get(); + + $this->assertEquals([1, 2], $rows->pluck('id')->all()); + } + + public function testWithExpressionMySql() + { + $builder = $this->getBuilder('MySql'); + $builder->select('u.id') + ->from('u') + ->withExpression('u', $this->getBuilder('MySql')->from('users')) + ->withExpression('p', $this->getBuilder('MySql')->from('posts')) + ->join('p', 'p.user_id', '=', 'u.id'); + + $expected = 'with `u` as (select * from `users`), `p` as (select * from `posts`) select `u`.`id` from `u` inner join `p` on `p`.`user_id` = `u`.`id`'; + $this->assertEquals($expected, $builder->toSql()); + } + + public function testWithExpressionPostgres() + { + $builder = $this->getBuilder('Postgres'); + $builder->select('u.id') + ->from('u') + ->withExpression('u', $this->getBuilder('Postgres')->from('users')) + ->withExpression('p', $this->getBuilder('Postgres')->from('posts')) + ->join('p', 'p.user_id', '=', 'u.id'); + + $expected = 'with "u" as (select * from "users"), "p" as (select * from "posts") select "u"."id" from "u" inner join "p" on "p"."user_id" = "u"."id"'; + $this->assertEquals($expected, $builder->toSql()); + } + + public function testWithExpressionSQLite() + { + $builder = $this->getBuilder('SQLite'); + $builder->select('u.id') + ->from('u') + ->withExpression('u', $this->getBuilder('SQLite')->from('users')) + ->withExpression('p', $this->getBuilder('SQLite')->from('posts')) + ->join('p', 'p.user_id', '=', 'u.id'); + + $expected = 'with "u" as (select * from "users"), "p" as (select * from "posts") select "u"."id" from "u" inner join "p" on "p"."user_id" = "u"."id"'; + $this->assertEquals($expected, $builder->toSql()); + } + + public function testWithExpressionSqlServer() + { + $builder = $this->getBuilder('SqlServer'); + $builder->select('u.id') + ->from('u') + ->withExpression('u', $this->getBuilder('SqlServer')->from('users')) + ->withExpression('p', $this->getBuilder('SqlServer')->from('posts')) + ->join('p', 'p.user_id', '=', 'u.id'); + + $expected = 'with [u] as (select * from [users]), [p] as (select * from [posts]) select [u].[id] from [u] inner join [p] on [p].[user_id] = [u].[id]'; + $this->assertEquals($expected, $builder->toSql()); + } + + public function testWithRecursiveExpression() + { + $query = 'select 1 union all select number + 1 from numbers where number < 3'; + + $rows = DB::table('numbers') + ->withRecursiveExpression('numbers', $query, ['number']) + ->get(); + + $this->assertEquals([1, 2, 3], $rows->pluck('number')->all()); + } + + public function testWithRecursiveExpressionMySql() + { + $query = $this->getBuilder('MySql')->selectRaw('1') + ->unionAll($this->getBuilder('MySql')->selectRaw('number + 1')->from('numbers')->where('number', '<', 3)); + $builder = $this->getBuilder('MySql'); + $builder->from('numbers') + ->withRecursiveExpression('numbers', $query, ['number']); + + $expected = 'with recursive `numbers` (`number`) as ('.$query->toSql().') select * from `numbers`'; + $this->assertEquals($expected, $builder->toSql()); + $this->assertEquals([3], $builder->getRawBindings()['expressions']); + } + + public function testWithRecursiveExpressionPostgres() + { + $query = $this->getBuilder('Postgres')->selectRaw('1') + ->unionAll($this->getBuilder('Postgres')->selectRaw('number + 1')->from('numbers')->where('number', '<', 3)); + $builder = $this->getBuilder('Postgres'); + $builder->from('numbers') + ->withRecursiveExpression('numbers', $query, ['number']); + + $expected = 'with recursive "numbers" ("number") as ('.$query->toSql().') select * from "numbers"'; + $this->assertEquals($expected, $builder->toSql()); + $this->assertEquals([3], $builder->getRawBindings()['expressions']); + } + + public function testWithRecursiveExpressionSQLite() + { + $query = $this->getBuilder('SQLite')->selectRaw('1') + ->unionAll($this->getBuilder('SQLite')->selectRaw('number + 1')->from('numbers')->where('number', '<', 3)); + $builder = $this->getBuilder('SQLite'); + $builder->from('numbers') + ->withRecursiveExpression('numbers', $query, ['number']); + + $expected = 'with recursive "numbers" ("number") as ('.$query->toSql().') select * from "numbers"'; + $this->assertEquals($expected, $builder->toSql()); + $this->assertEquals([3], $builder->getRawBindings()['expressions']); + } + + public function testWithRecursiveExpressionSqlServer() + { + $query = $this->getBuilder('SqlServer')->selectRaw('1') + ->unionAll($this->getBuilder('SqlServer')->selectRaw('number + 1')->from('numbers')->where('number', '<', 3)); + $builder = $this->getBuilder('SqlServer'); + $builder->from('numbers') + ->withRecursiveExpression('numbers', $query, ['number']); + + $expected = 'with [numbers] ([number]) as ('.$query->toSql().') select * from [numbers]'; + $this->assertEquals($expected, $builder->toSql()); + $this->assertEquals([3], $builder->getRawBindings()['expressions']); + } + + public function testInsertUsing() + { + if (! method_exists(BaseBuilder::class, 'insertUsing')) { + $this->markTestSkipped(); + } + + DB::table('posts') + ->withExpression('u', DB::table('users')->select('id')->where('id', '>', 1)) + ->insertUsing(['user_id'], DB::table('u')); + + $this->assertEquals([1, 2, 2, 3], DB::table('posts')->pluck('user_id')->all()); + } + + public function testUpdate() + { + if (Str::contains(DB::getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION), 'MariaDB')) { + $this->markTestSkipped(); + } + + DB::table('posts') + ->withExpression('u', DB::table('users')->where('id', '>', 1)) + ->update(['user_id' => DB::raw('(select min(id) from u)')]); + + $this->assertEquals([2, 2], DB::table('posts')->pluck('user_id')->all()); + } + + public function testDelete() + { + if (Str::contains(DB::getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION), 'MariaDB')) { + $this->markTestSkipped(); + } + + DB::table('posts') + ->withExpression('u', DB::table('users')->where('id', '>', 1)) + ->whereIn('user_id', DB::table('u')->select('id')) + ->delete(); + + $this->assertEquals([1], DB::table('posts')->pluck('user_id')->all()); + } + + protected function getBuilder($database) + { + $connection = $this->createMock(Connection::class); + $connection->method('getPdo')->willReturn($this->createMock(PDO::class)); + $grammar = 'Staudenmeir\LaravelCte\Grammars\\'.$database.'Grammar'; + $processor = $this->createMock(Processor::class); + + return new Builder($connection, new $grammar, $processor); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..ddbae8e --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,53 @@ +increments('id'); + $table->unsignedInteger('parent_id')->nullable(); + }); + + Schema::create('posts', function (Blueprint $table) { + $table->increments('id'); + $table->unsignedInteger('user_id'); + }); + + DB::table('users')->insert([ + ['parent_id' => null], + ['parent_id' => 1], + ['parent_id' => 2], + ]); + DB::table('posts')->insert([ + ['user_id' => 1], + ['user_id' => 2], + ]); + } + + protected function getEnvironmentSetUp($app) + { + $config = require __DIR__.'/config/database.php'; + + $app['config']->set('database.default', 'testing'); + + $app['config']->set('database.connections.testing', $config[getenv('DB') ?: 'sqlite']); + } + + protected function getPackageProviders($app) + { + return [DatabaseServiceProvider::class]; + } +} diff --git a/tests/config/database.php b/tests/config/database.php new file mode 100644 index 0000000..48886a8 --- /dev/null +++ b/tests/config/database.php @@ -0,0 +1,35 @@ + [ + 'driver' => 'mysql', + 'host' => '127.0.0.1', + 'port' => '3306', + 'database' => 'homestead', + 'username' => 'homestead', + 'password' => 'secret', + 'unix_socket' => '', + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + 'strict' => true, + 'engine' => null, + ], + 'pgsql' => [ + 'driver' => 'pgsql', + 'host' => '127.0.0.1', + 'port' => '5432', + 'database' => 'homestead', + 'username' => 'homestead', + 'password' => 'secret', + 'charset' => 'utf8', + 'prefix' => '', + 'schema' => 'public', + 'sslmode' => 'prefer', + ], + 'sqlite' => [ + 'driver' => 'sqlite', + 'database' => ':memory:', + 'prefix' => '', + ], +]; diff --git a/tests/config/database.travis.php b/tests/config/database.travis.php new file mode 100644 index 0000000..75f5e7a --- /dev/null +++ b/tests/config/database.travis.php @@ -0,0 +1,35 @@ + [ + 'driver' => 'mysql', + 'host' => '127.0.0.1', + 'port' => '3306', + 'database' => 'test', + 'username' => 'root', + 'password' => '', + 'unix_socket' => '', + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + 'strict' => true, + 'engine' => null, + ], + 'pgsql' => [ + 'driver' => 'pgsql', + 'host' => '127.0.0.1', + 'port' => '5432', + 'database' => 'test', + 'username' => 'postgres', + 'password' => '', + 'charset' => 'utf8', + 'prefix' => '', + 'schema' => 'public', + 'sslmode' => 'prefer', + ], + 'sqlite' => [ + 'driver' => 'sqlite', + 'database' => ':memory:', + 'prefix' => '', + ], +];