diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 4f67e04..a2c2e1b 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -15,11 +15,13 @@ jobs: fail-fast: false matrix: os: [ ubuntu-latest ] - php: [ 8.1, 8.2, 8.3 ] - laravel: [ 10.*, 9.* ] - cockroachdb: [ v22.2.17, v23.1.13 ] + php: [ 8.2, 8.3 ] + laravel: [ 11.*, 10.*, 9.* ] + cockroachdb: [ v22.2.17, v23.2.2 ] dependencies: [ stable, lowest ] include: + - laravel: 11.* + testbench: ^9.0 - laravel: 10.* testbench: ^8.0 - laravel: 9.* diff --git a/composer.json b/composer.json index fcb9933..13b9634 100644 --- a/composer.json +++ b/composer.json @@ -17,22 +17,22 @@ } ], "require": { - "php": "^8.1", + "php": "^8.2", "spatie/laravel-package-tools": "^1.9.2", - "illuminate/contracts": "10.*|9.*|8.*" + "illuminate/contracts": "11.*|10.*|9.*" }, "require-dev": { "doctrine/dbal": "^3.2", "laravel/pint": "^1.2", - "nunomaduro/collision": "7.2|^6.0|^5.10", - "nunomaduro/larastan": "^2.0|^1.0", - "orchestra/testbench": "^8.0|^7.0|^6.24", + "nunomaduro/collision": "^8.1|7.2|^6.0|^5.10", + "larastan/larastan": "^2.0", + "orchestra/testbench": "^9.0|^8.0|^7.0", "pestphp/pest": "^1.21|^2.23", "pestphp/pest-plugin-laravel": "^1.1|^2.2", "phpstan/extension-installer": "^1.1", "phpstan/phpstan-deprecation-rules": "^1.0", "phpstan/phpstan-phpunit": "^1.0", - "rector/rector": "^0.14.2", + "rector/rector": "^0.14.2|^1.0", "spatie/laravel-ray": "^1.26" }, "autoload": { diff --git a/database.php b/database.php index 8c1e7a0..7b27b2d 100644 --- a/database.php +++ b/database.php @@ -25,15 +25,14 @@ heredoc); $statements = [ - 'SET CLUSTER SETTING kv.raft_log.disable_synchronization_unsafe = true;', 'SET CLUSTER SETTING kv.range_merge.queue_interval = \'50ms\';', 'SET CLUSTER SETTING jobs.registry.interval.gc = \'30s\';', 'SET CLUSTER SETTING jobs.registry.interval.cancel = \'180s\';', 'SET CLUSTER SETTING jobs.retention_time = \'15s\';', 'SET CLUSTER SETTING sql.stats.automatic_collection.enabled = false;', 'SET CLUSTER SETTING kv.range_split.by_load_merge_delay = \'5s\';', - 'ALTER RANGE default CONFIGURE ZONE USING "gc.ttlseconds" = 5;', - 'ALTER DATABASE system CONFIGURE ZONE USING "gc.ttlseconds" = 5;', + 'ALTER RANGE default CONFIGURE ZONE USING "gc.ttlseconds" = 600;', + 'ALTER DATABASE system CONFIGURE ZONE USING "gc.ttlseconds" = 600;', ]; foreach ($statements as $statement) { diff --git a/docker-compose.yml b/docker-compose.yml index e8d9879..7fc3d9a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: '3' services: crdb: - image: "cockroachdb/cockroach:${VERSION:-v23.1.13}" + image: "cockroachdb/cockroach:${VERSION:-v23.2.2}" ports: - "26257:26257" - "8080:8080" diff --git a/src/CockroachDbConnection.php b/src/CockroachDbConnection.php index 456dc83..0b0d1cb 100644 --- a/src/CockroachDbConnection.php +++ b/src/CockroachDbConnection.php @@ -10,7 +10,7 @@ use YlsIdeas\CockroachDb\Builder\CockroachDbBuilder as DbBuilder; use YlsIdeas\CockroachDb\Processor\CockroachDbProcessor as DbProcessor; use YlsIdeas\CockroachDb\Query\CockroachGrammar as QueryGrammar; -use YlsIdeas\CockroachDb\Schema\CockroachGrammar as SchemaGrammar; +use YlsIdeas\CockroachDb\Schema\CockroachDbGrammar as SchemaGrammar; use YlsIdeas\CockroachDb\Schema\CockroachSchemaState as SchemaState; class CockroachDbConnection extends PostgresConnection implements ConnectionInterface @@ -73,9 +73,11 @@ protected function getDefaultPostProcessor(): DbProcessor * Get the Doctrine DBAL driver. * * @return \Illuminate\Database\PDO\PostgresDriver + * @phpstan-ignore-next-line Missing in Laravel 11 */ protected function getDoctrineDriver() { + /** @phpstan-ignore-next-line Now redundant in Laravel 11 */ return new PostgresDriver(); } diff --git a/src/Processor/CockroachDbProcessor.php b/src/Processor/CockroachDbProcessor.php index dcb42d3..eac40df 100644 --- a/src/Processor/CockroachDbProcessor.php +++ b/src/Processor/CockroachDbProcessor.php @@ -6,4 +6,30 @@ class CockroachDbProcessor extends PostgresProcessor { + public function processColumns($results) + { + return array_map(function ($result) { + $result = (object) $result; + + $autoincrement = $result->default !== null && str_starts_with($result->default, 'nextval('); + + return [ + 'name' => $result->name, + 'type_name' => $result->type_name, + 'type' => $result->type, + 'collation' => $result->collation, + 'nullable' => (bool) $result->nullable, + 'default' => ($result->generated ?? null) ? null : $result->default, + 'auto_increment' => $autoincrement, + 'comment' => $result->comment, + 'generation' => ($result->generated ?? null) ? [ + 'type' => match ($result->generated) { + 's' => 'stored', + default => null, + }, + 'expression' => $result->default, + ] : null, + ]; + }, $results); + } } diff --git a/src/Schema/CockroachGrammar.php b/src/Schema/CockroachDbGrammar.php similarity index 97% rename from src/Schema/CockroachGrammar.php rename to src/Schema/CockroachDbGrammar.php index 273fab7..26f6388 100644 --- a/src/Schema/CockroachGrammar.php +++ b/src/Schema/CockroachDbGrammar.php @@ -7,7 +7,7 @@ use Illuminate\Support\Fluent; use YlsIdeas\CockroachDb\Exceptions\FeatureNotSupportedException; -class CockroachGrammar extends PostgresGrammar +class CockroachDbGrammar extends PostgresGrammar { /** * Compile the query to determine the tables. diff --git a/tests/Database/DatabaseCockroachDbBuilderTest.php b/tests/Database/DatabaseCockroachDbBuilderTest.php index 9e62411..81cd822 100644 --- a/tests/Database/DatabaseCockroachDbBuilderTest.php +++ b/tests/Database/DatabaseCockroachDbBuilderTest.php @@ -6,11 +6,14 @@ use Mockery as m; use PHPUnit\Framework\TestCase; use YlsIdeas\CockroachDb\Builder\CockroachDbBuilder; -use YlsIdeas\CockroachDb\Schema\CockroachGrammar; +use YlsIdeas\CockroachDb\Processor\CockroachDbProcessor; +use YlsIdeas\CockroachDb\Schema\CockroachDbGrammar; +use YlsIdeas\CockroachDb\Tests\WithMultipleApplicationVersions; class DatabaseCockroachDbBuilderTest extends TestCase { use m\Adapter\Phpunit\MockeryPHPUnitIntegration; + use WithMultipleApplicationVersions; protected function tearDown(): void { @@ -19,9 +22,9 @@ protected function tearDown(): void public function test_create_database() { - $grammar = new CockroachGrammar(); + $grammar = new CockroachDbGrammar(); - $connection = m::mock(Connection::class); + $connection = $this->getConnection(); $connection->shouldReceive('getConfig')->once()->with('charset')->andReturn('utf8'); $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); $connection->shouldReceive('statement')->once()->with( @@ -34,9 +37,9 @@ public function test_create_database() public function test_drop_database_if_exists() { - $grammar = new CockroachGrammar(); + $grammar = new CockroachDbGrammar(); - $connection = m::mock(Connection::class); + $connection = $this->getConnection(); $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); $connection->shouldReceive('statement')->once()->with( 'drop database if exists "my_database_a"' @@ -47,8 +50,312 @@ public function test_drop_database_if_exists() $builder->dropDatabaseIfExists('my_database_a'); } + public function test_has_table_when_schema_unqualified_and_search_path_missing() + { + $this->skipIfOlderThan('10.0.0'); + + $connection = $this->getConnection(); + $connection->shouldReceive('getConfig')->with('search_path')->andReturn(null); + $connection->shouldReceive('getConfig')->with('schema')->andReturn(null); + $grammar = m::mock(CockroachDbGrammar::class); + $processor = m::mock(CockroachDbProcessor::class); + $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); + $grammar->shouldReceive('compileTableExists')->andReturn("select * from information_schema.tables where table_catalog = ? and table_schema = ? and table_name = ? and table_type = 'BASE TABLE'"); + $connection->shouldReceive('selectFromWriteConnection')->with("select * from information_schema.tables where table_catalog = ? and table_schema = ? and table_name = ? and table_type = 'BASE TABLE'", ['laravel', 'public', 'foo'])->andReturn(['countable_result']); + $connection->shouldReceive('getPostProcessor')->andReturn($processor); + $grammar->shouldReceive('compileTables')->andReturn('sql'); + $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['schema' => 'public', 'name' => 'foo']]); + $connection->shouldReceive('getTablePrefix'); + $connection->shouldReceive('getConfig')->with('database')->andReturn('laravel'); + $builder = $this->getBuilder($connection); + $processor->shouldReceive('processTables')->andReturn([['schema' => 'public', 'name' => 'foo']]); + + $builder->hasTable('foo'); + $this->assertTrue($builder->hasTable('foo')); + $this->assertTrue($builder->hasTable('public.foo')); + } + + public function test_has_table_when_schema_unqualified_and_search_path_filled() + { + $this->skipIfOlderThan('11.0.0'); + + $connection = $this->getConnection(); + $connection->shouldReceive('getConfig')->with('search_path')->andReturn('myapp,public'); + $grammar = m::mock(CockroachDbGrammar::class); + $processor = m::mock(CockroachDbProcessor::class); + $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); + $connection->shouldReceive('getPostProcessor')->andReturn($processor); + $grammar->shouldReceive('compileTables')->andReturn('sql'); + $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['schema' => 'myapp', 'name' => 'foo']]); + $connection->shouldReceive('getTablePrefix'); + $builder = $this->getBuilder($connection); + $processor->shouldReceive('processTables')->andReturn([['schema' => 'myapp', 'name' => 'foo']]); + + $this->assertTrue($builder->hasTable('foo')); + $this->assertTrue($builder->hasTable('myapp.foo')); + } + + public function test_has_table_when_schema_unqualified_and_search_path_fallback_filled() + { + $this->skipIfOlderThan('11.0.0'); + + $connection = $this->getConnection(); + $connection->shouldReceive('getConfig')->with('search_path')->andReturn(null); + $connection->shouldReceive('getConfig')->with('schema')->andReturn(['myapp', 'public']); + $grammar = m::mock(CockroachDbGrammar::class); + $processor = m::mock(CockroachDbProcessor::class); + $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); + $connection->shouldReceive('getPostProcessor')->andReturn($processor); + $grammar->shouldReceive('compileTables')->andReturn('sql'); + $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['schema' => 'myapp', 'name' => 'foo']]); + $connection->shouldReceive('getTablePrefix'); + $builder = $this->getBuilder($connection); + $processor->shouldReceive('processTables')->andReturn([['schema' => 'myapp', 'name' => 'foo']]); + + $this->assertTrue($builder->hasTable('foo')); + $this->assertTrue($builder->hasTable('myapp.foo')); + } + + public function test_has_table_when_schema_unqualified_and_search_path_is_user_variable() + { + $this->skipIfOlderThan('11.0.0'); + + $connection = $this->getConnection(); + $connection->shouldReceive('getConfig')->with('username')->andReturn('foouser'); + $connection->shouldReceive('getConfig')->with('search_path')->andReturn('$user'); + $grammar = m::mock(CockroachDbGrammar::class); + $processor = m::mock(CockroachDbProcessor::class); + $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); + $connection->shouldReceive('getPostProcessor')->andReturn($processor); + $grammar->shouldReceive('compileTables')->andReturn('sql'); + $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['schema' => 'foouser', 'name' => 'foo']]); + $connection->shouldReceive('getTablePrefix'); + $builder = $this->getBuilder($connection); + $processor->shouldReceive('processTables')->andReturn([['schema' => 'foouser', 'name' => 'foo']]); + + $this->assertTrue($builder->hasTable('foo')); + $this->assertTrue($builder->hasTable('foouser.foo')); + } + + public function test_has_table_when_schema_qualified_and_search_path_mismatches() + { + $this->skipIfOlderThan('11.0.0'); + + $connection = $this->getConnection(); + $connection->shouldReceive('getConfig')->with('search_path')->andReturn('public'); + $grammar = m::mock(CockroachDbGrammar::class); + $processor = m::mock(CockroachDbProcessor::class); + $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); + $connection->shouldReceive('getPostProcessor')->andReturn($processor); + $grammar->shouldReceive('compileTables')->andReturn('sql'); + $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['schema' => 'myapp', 'name' => 'foo']]); + $connection->shouldReceive('getTablePrefix'); + $builder = $this->getBuilder($connection); + $processor->shouldReceive('processTables')->andReturn([['schema' => 'myapp', 'name' => 'foo']]); + + $this->assertTrue($builder->hasTable('myapp.foo')); + } + + public function test_has_table_when_database_and_schema_qualified_and_search_path_mismatches() + { + $this->skipIfOlderThan('11.0.0'); + + $this->expectException(\InvalidArgumentException::class); + + $connection = $this->getConnection(); + $grammar = m::mock(CockroachDbGrammar::class); + $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); + $builder = $this->getBuilder($connection); + + $builder->hasTable('mydatabase.myapp.foo'); + } + + public function test_get_column_listing_when_schema_unqualified_and_search_path_missing() + { + $this->skipIfOlderThan('11.0.0'); + + $connection = $this->getConnection(); + $connection->shouldReceive('getConfig')->with('search_path')->andReturn(null); + $connection->shouldReceive('getConfig')->with('schema')->andReturn(null); + $grammar = m::mock(CockroachDbGrammar::class); + $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); + $grammar->shouldReceive('compileColumns')->with('public', 'foo')->andReturn('sql'); + $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['name' => 'some_column']]); + $connection->shouldReceive('getTablePrefix'); + $processor = m::mock(CockroachDbProcessor::class); + $connection->shouldReceive('getPostProcessor')->andReturn($processor); + $processor->shouldReceive('processColumns')->andReturn([['name' => 'some_column']]); + $builder = $this->getBuilder($connection); + + $builder->getColumnListing('foo'); + } + + public function test_get_column_listing_when_schema_unqualified_and_search_path_filled() + { + $this->skipIfOlderThan('11.0.0'); + + $connection = $this->getConnection(); + $connection->shouldReceive('getConfig')->with('search_path')->andReturn('myapp,public'); + $grammar = m::mock(CockroachDbGrammar::class); + $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); + $grammar->shouldReceive('compileColumns')->with('myapp', 'foo')->andReturn('sql'); + $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['name' => 'some_column']]); + $connection->shouldReceive('getTablePrefix'); + $processor = m::mock(CockroachDbProcessor::class); + $connection->shouldReceive('getPostProcessor')->andReturn($processor); + $processor->shouldReceive('processColumns')->andReturn([['name' => 'some_column']]); + $builder = $this->getBuilder($connection); + + $builder->getColumnListing('foo'); + } + + public function test_get_column_listing_when_schema_unqualified_and_search_path_is_user_variable() + { + $this->skipIfOlderThan('11.0.0'); + + $connection = $this->getConnection(); + $connection->shouldReceive('getConfig')->with('username')->andReturn('foouser'); + $connection->shouldReceive('getConfig')->with('search_path')->andReturn('$user'); + $grammar = m::mock(CockroachDbGrammar::class); + $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); + $grammar->shouldReceive('compileColumns')->with('foouser', 'foo')->andReturn('sql'); + $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['name' => 'some_column']]); + $connection->shouldReceive('getTablePrefix'); + $processor = m::mock(CockroachDbProcessor::class); + $connection->shouldReceive('getPostProcessor')->andReturn($processor); + $processor->shouldReceive('processColumns')->andReturn([['name' => 'some_column']]); + $builder = $this->getBuilder($connection); + + $builder->getColumnListing('foo'); + } + + public function test_get_column_listing_when_schema_qualified_and_search_path_mismatches() + { + $this->skipIfOlderThan('11.0.0'); + + $connection = $this->getConnection(); + $connection->shouldReceive('getConfig')->with('search_path')->andReturn('public'); + $grammar = m::mock(CockroachDbGrammar::class); + $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); + $grammar->shouldReceive('compileColumns')->with('myapp', 'foo')->andReturn('sql'); + $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['name' => 'some_column']]); + $connection->shouldReceive('getTablePrefix'); + $processor = m::mock(CockroachDbProcessor::class); + $connection->shouldReceive('getPostProcessor')->andReturn($processor); + $processor->shouldReceive('processColumns')->andReturn([['name' => 'some_column']]); + $builder = $this->getBuilder($connection); + + $builder->getColumnListing('myapp.foo'); + } + + public function test_get_column_when_database_and_schema_qualified_and_search_path_mismatches() + { + $this->skipIfOlderThan('11.0.0'); + + $this->expectException(\InvalidArgumentException::class); + + $connection = $this->getConnection(); + $connection->shouldReceive('getConfig')->with('search_path')->andReturn('public'); + $grammar = m::mock(CockroachDbGrammar::class); + $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); + $builder = $this->getBuilder($connection); + + $builder->getColumnListing('mydatabase.myapp.foo'); + } + + public function test_drop_all_tables_when_search_path_is_string() + { + $this->skipIfOlderThan('11.0.0'); + + $connection = $this->getConnection(); + $connection->shouldReceive('getConfig')->with('search_path')->andReturn('public'); + $connection->shouldReceive('getConfig')->with('dont_drop')->andReturn(['foo']); + $grammar = m::mock(CockroachDbGrammar::class); + $processor = m::mock(CockroachDbProcessor::class); + $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); + $connection->shouldReceive('getPostProcessor')->andReturn($processor); + $grammar->shouldReceive('compileTables')->andReturn('sql'); + $processor->shouldReceive('processTables')->once()->andReturn([['name' => 'users', 'schema' => 'public']]); + $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['name' => 'users', 'schema' => 'public']]); + $grammar->shouldReceive('escapeNames')->with(['public'])->andReturn(['"public"']); + $grammar->shouldReceive('escapeNames')->with(['foo'])->andReturn(['"foo"']); + $grammar->shouldReceive('escapeNames')->with(['users', 'public.users'])->andReturn(['"users"', '"public"."users"']); + $grammar->shouldReceive('compileDropAllTables')->with(['public.users'])->andReturn('drop table "public"."users" cascade'); + $connection->shouldReceive('statement')->with('drop table "public"."users" cascade'); + $builder = $this->getBuilder($connection); + + $builder->dropAllTables(); + } + + public function test_drop_all_tables_when_search_path_is_string_of_many() + { + $this->skipIfOlderThan('11.0.0'); + + $connection = $this->getConnection(); + $connection->shouldReceive('getConfig')->with('username')->andReturn('foouser'); + $connection->shouldReceive('getConfig')->with('search_path')->andReturn('"$user", public, foo_bar-Baz.Áüõß'); + $connection->shouldReceive('getConfig')->with('dont_drop')->andReturn(['foo']); + $grammar = m::mock(CockroachDbGrammar::class); + $processor = m::mock(CockroachDbProcessor::class); + $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); + $connection->shouldReceive('getPostProcessor')->andReturn($processor); + $processor->shouldReceive('processTables')->once()->andReturn([['name' => 'users', 'schema' => 'foouser']]); + $grammar->shouldReceive('compileTables')->andReturn('sql'); + $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['name' => 'users', 'schema' => 'foouser']]); + $grammar->shouldReceive('escapeNames')->with(['foouser', 'public', 'foo_bar-Baz.Áüõß'])->andReturn(['"foouser"', '"public"', '"foo_bar-Baz"."Áüõß"']); + $grammar->shouldReceive('escapeNames')->with(['foo'])->andReturn(['"foo"']); + $grammar->shouldReceive('escapeNames')->with(['foouser'])->andReturn(['"foouser"']); + $grammar->shouldReceive('escapeNames')->with(['users', 'foouser.users'])->andReturn(['"users"', '"foouser"."users"']); + $grammar->shouldReceive('compileDropAllTables')->with(['foouser.users'])->andReturn('drop table "foouser"."users" cascade'); + $connection->shouldReceive('statement')->with('drop table "foouser"."users" cascade'); + $builder = $this->getBuilder($connection); + + $builder->dropAllTables(); + } + + public function test_drop_all_tables_when_search_path_is_array_of_many() + { + $this->skipIfOlderThan('11.0.0'); + + $connection = $this->getConnection(); + $connection->shouldReceive('getConfig')->with('username')->andReturn('foouser'); + $connection->shouldReceive('getConfig')->with('search_path')->andReturn([ + '$user', + '"dev"', + "'test'", + 'spaced schema', + ]); + $connection->shouldReceive('getConfig')->with('dont_drop')->andReturn(['foo']); + $grammar = m::mock(CockroachDbGrammar::class); + $processor = m::mock(CockroachDbProcessor::class); + $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); + $connection->shouldReceive('getPostProcessor')->andReturn($processor); + $processor->shouldReceive('processTables')->once()->andReturn([['name' => 'users', 'schema' => 'foouser']]); + $grammar->shouldReceive('compileTables')->andReturn('sql'); + $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn([['name' => 'users', 'schema' => 'foouser']]); + $grammar->shouldReceive('escapeNames')->with(['foouser', 'dev', 'test', 'spaced schema'])->andReturn(['"foouser"', '"dev"', '"test"', '"spaced schema"']); + $grammar->shouldReceive('escapeNames')->with(['foo'])->andReturn(['"foo"']); + $grammar->shouldReceive('escapeNames')->with(['users', 'foouser.users'])->andReturn(['"users"', '"foouser"."users"']); + $grammar->shouldReceive('escapeNames')->with(['foouser'])->andReturn(['"foouser"']); + $grammar->shouldReceive('compileDropAllTables')->with(['foouser.users'])->andReturn('drop table "foouser"."users" cascade'); + $connection->shouldReceive('statement')->with('drop table "foouser"."users" cascade'); + $builder = $this->getBuilder($connection); + + $builder->dropAllTables(); + } + + protected function getConnection() + { + return m::mock(Connection::class); + } + protected function getBuilder($connection) { return new CockroachDbBuilder($connection); } + + protected function getGrammar() + { + return new CockroachDbGrammar(); + } } diff --git a/tests/Database/DatabaseCockroachDbProcessorTest.php b/tests/Database/Laravel10/DatabaseCockroachDbProcessorTest.php similarity index 78% rename from tests/Database/DatabaseCockroachDbProcessorTest.php rename to tests/Database/Laravel10/DatabaseCockroachDbProcessorTest.php index 907e7e9..1da6038 100644 --- a/tests/Database/DatabaseCockroachDbProcessorTest.php +++ b/tests/Database/Laravel10/DatabaseCockroachDbProcessorTest.php @@ -1,14 +1,19 @@ skipIfNewerThan('11.0.0'); + $processor = new CockroachDbProcessor(); $listing = [['column_name' => 'id'], ['column_name' => 'name'], ['column_name' => 'email']]; diff --git a/tests/Database/Laravel10/DatabaseCockroachDbSchemaGrammarTest.php b/tests/Database/Laravel10/DatabaseCockroachDbSchemaGrammarTest.php index 1057b7a..883d354 100644 --- a/tests/Database/Laravel10/DatabaseCockroachDbSchemaGrammarTest.php +++ b/tests/Database/Laravel10/DatabaseCockroachDbSchemaGrammarTest.php @@ -9,7 +9,7 @@ use Mockery as m; use PHPUnit\Framework\TestCase; use YlsIdeas\CockroachDb\Exceptions\FeatureNotSupportedException; -use YlsIdeas\CockroachDb\Schema\CockroachGrammar; +use YlsIdeas\CockroachDb\Schema\CockroachDbGrammar; use YlsIdeas\CockroachDb\Tests\WithMultipleApplicationVersions; class DatabaseCockroachDbSchemaGrammarTest extends TestCase @@ -22,6 +22,7 @@ class DatabaseCockroachDbSchemaGrammarTest extends TestCase public function onlyForLaravel10() { $this->skipIfOlderThan('10.0.0'); + $this->skipIfNewerThan('11.0.0'); } protected function tearDown(): void @@ -1099,7 +1100,18 @@ protected function getConnection() public function getGrammar() { - return new CockroachGrammar(); + return new CockroachDbGrammar(); + } + + public function test_compile_columns() + { + if (! method_exists($this->getGrammar(), 'compileColumns')) { + $this->markTestSkipped('Installed Laravel Version does not have compileColumns() method'); + } + + $statement = $this->getGrammar()->compileColumns('db', 'public', 'table'); + + $this->assertStringContainsString("where c.relname = 'table' and n.nspname = 'public'", $statement); } public function test_grammars_are_macroable() diff --git a/tests/Database/Laravel11/DatabaseCockroachDbProcessorTest.php b/tests/Database/Laravel11/DatabaseCockroachDbProcessorTest.php new file mode 100644 index 0000000..322bd3e --- /dev/null +++ b/tests/Database/Laravel11/DatabaseCockroachDbProcessorTest.php @@ -0,0 +1,40 @@ +skipIfOlderThan('11.0.0'); + $processor = new CockroachDbProcessor(); + + $listing = [ + ['name' => 'id', 'type_name' => 'int4', 'type' => 'integer', 'collation' => '', 'nullable' => true, 'default' => "nextval('employee_id_seq'::regclass)", 'comment' => ''], + ['name' => 'name', 'type_name' => 'varchar', 'type' => 'character varying(100)', 'collation' => 'collate', 'nullable' => false, 'default' => '', 'comment' => 'foo'], + ['name' => 'balance', 'type_name' => 'numeric', 'type' => 'numeric(8,2)', 'collation' => '', 'nullable' => true, 'default' => '4', 'comment' => 'NULL'], + ['name' => 'birth_date', 'type_name' => 'timestamp', 'type' => 'timestamp(6) without time zone', 'collation' => '', 'nullable' => false, 'default' => '', 'comment' => ''], + ]; + $expected = [ + ['name' => 'id', 'type_name' => 'int4', 'type' => 'integer', 'collation' => '', 'nullable' => true, 'default' => "nextval('employee_id_seq'::regclass)", 'auto_increment' => true, 'comment' => '', 'generation' => null], + ['name' => 'name', 'type_name' => 'varchar', 'type' => 'character varying(100)', 'collation' => 'collate', 'nullable' => false, 'default' => '', 'auto_increment' => false, 'comment' => 'foo', 'generation' => null], + ['name' => 'balance', 'type_name' => 'numeric', 'type' => 'numeric(8,2)', 'collation' => '', 'nullable' => true, 'default' => '4', 'auto_increment' => false, 'comment' => 'NULL', 'generation' => null], + ['name' => 'birth_date', 'type_name' => 'timestamp', 'type' => 'timestamp(6) without time zone', 'collation' => '', 'nullable' => false, 'default' => '', 'auto_increment' => false, 'comment' => '', 'generation' => null], + ]; + + $this->assertEquals($expected, $processor->processColumns($listing)); + + // convert listing to objects to simulate PDO::FETCH_CLASS + foreach ($listing as &$row) { + $row = (object) $row; + } + + $this->assertEquals($expected, $processor->processColumns($listing)); + } +} diff --git a/tests/Database/Laravel11/DatabaseCockroachDbSchemaGrammarTest.php b/tests/Database/Laravel11/DatabaseCockroachDbSchemaGrammarTest.php new file mode 100644 index 0000000..0a2b49a --- /dev/null +++ b/tests/Database/Laravel11/DatabaseCockroachDbSchemaGrammarTest.php @@ -0,0 +1,1116 @@ +skipIfOlderThan('11.0.0'); + } + + protected function tearDown(): void + { + m::close(); + } + + public function test_basic_create_table() + { + $blueprint = new Blueprint('users'); + $blueprint->create(); + $blueprint->increments('id'); + $blueprint->string('email'); + $blueprint->string('name')->collation('nb_NO.utf8'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('create table "users" ("id" serial not null primary key, "email" varchar(255) not null, "name" varchar(255) collate "nb_NO.utf8" not null)', $statements[0]); + + $blueprint = new Blueprint('users'); + $blueprint->increments('id'); + $blueprint->string('email'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "id" serial not null primary key, add column "email" varchar(255) not null', $statements[0]); + } + + public function test_create_table_with_auto_increment_starting_value() + { + $blueprint = new Blueprint('users'); + $blueprint->create(); + $blueprint->increments('id')->startingValue(1000); + $blueprint->string('email'); + $blueprint->string('name')->collation('nb_NO.utf8'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(2, $statements); + $this->assertSame('create table "users" ("id" serial not null primary key, "email" varchar(255) not null, "name" varchar(255) collate "nb_NO.utf8" not null)', $statements[0]); + $this->assertSame('alter sequence users_id_seq restart with 1000', $statements[1]); + } + + public function test_create_table_and_comment_column() + { + $blueprint = new Blueprint('users'); + $blueprint->create(); + $blueprint->increments('id'); + $blueprint->string('email')->comment('my first comment'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(2, $statements); + $this->assertSame('create table "users" ("id" serial not null primary key, "email" varchar(255) not null)', $statements[0]); + $this->assertSame('comment on column "users"."email" is \'my first comment\'', $statements[1]); + } + + public function test_create_temporary_table() + { + $blueprint = new Blueprint('users'); + $blueprint->create(); + $blueprint->temporary(); + $blueprint->increments('id'); + $blueprint->string('email'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('create temporary table "users" ("id" serial not null primary key, "email" varchar(255) not null)', $statements[0]); + } + + public function test_drop_table() + { + $blueprint = new Blueprint('users'); + $blueprint->drop(); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('drop table "users"', $statements[0]); + } + + public function test_drop_table_if_exists() + { + $blueprint = new Blueprint('users'); + $blueprint->dropIfExists(); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('drop table if exists "users"', $statements[0]); + } + + public function test_drop_column() + { + $blueprint = new Blueprint('users'); + $blueprint->dropColumn('foo'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" drop column "foo"', $statements[0]); + + $blueprint = new Blueprint('users'); + $blueprint->dropColumn(['foo', 'bar']); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" drop column "foo", drop column "bar"', $statements[0]); + + $blueprint = new Blueprint('users'); + $blueprint->dropColumn('foo', 'bar'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" drop column "foo", drop column "bar"', $statements[0]); + } + + public function test_drop_primary() + { + $blueprint = new Blueprint('users'); + $blueprint->dropPrimary(); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" drop constraint "users_pkey"', $statements[0]); + } + + public function test_drop_unique() + { + $blueprint = new Blueprint('users'); + $blueprint->dropUnique('foo'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('drop index "users"@"foo" cascade', $statements[0]); + } + + public function test_drop_index() + { + $blueprint = new Blueprint('users'); + $blueprint->dropIndex('foo'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('drop index "foo"', $statements[0]); + } + + public function test_drop_spatial_index() + { + $blueprint = new Blueprint('geo'); + $blueprint->dropSpatialIndex(['coordinates']); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('drop index "geo_coordinates_spatialindex"', $statements[0]); + } + + public function test_drop_foreign() + { + $blueprint = new Blueprint('users'); + $blueprint->dropForeign('foo'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" drop constraint "foo"', $statements[0]); + } + + public function test_drop_timestamps() + { + $blueprint = new Blueprint('users'); + $blueprint->dropTimestamps(); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" drop column "created_at", drop column "updated_at"', $statements[0]); + } + + public function test_drop_timestamps_tz() + { + $blueprint = new Blueprint('users'); + $blueprint->dropTimestampsTz(); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" drop column "created_at", drop column "updated_at"', $statements[0]); + } + + public function test_drop_morphs() + { + $blueprint = new Blueprint('photos'); + $blueprint->dropMorphs('imageable'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(2, $statements); + $this->assertSame('drop index "photos_imageable_type_imageable_id_index"', $statements[0]); + $this->assertSame('alter table "photos" drop column "imageable_type", drop column "imageable_id"', $statements[1]); + } + + public function test_rename_table() + { + $blueprint = new Blueprint('users'); + $blueprint->rename('foo'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" rename to "foo"', $statements[0]); + } + + public function test_rename_index() + { + $blueprint = new Blueprint('users'); + $blueprint->renameIndex('foo', 'bar'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter index "foo" rename to "bar"', $statements[0]); + } + + public function test_adding_primary_key() + { + $blueprint = new Blueprint('users'); + $blueprint->primary('foo'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add primary key ("foo")', $statements[0]); + } + + public function test_adding_unique_key() + { + $blueprint = new Blueprint('users'); + $blueprint->unique('foo', 'bar'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add constraint "bar" unique ("foo")', $statements[0]); + } + + public function test_adding_index() + { + $blueprint = new Blueprint('users'); + $blueprint->index(['foo', 'bar'], 'baz'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('create index "baz" on "users" ("foo", "bar")', $statements[0]); + } + + public function test_adding_index_with_algorithm() + { + $blueprint = new Blueprint('users'); + $blueprint->index(['foo', 'bar'], 'baz', 'hash'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('create index "baz" on "users" using hash ("foo", "bar")', $statements[0]); + } + + public function test_adding_spatial_index() + { + $blueprint = new Blueprint('geo'); + $blueprint->spatialIndex('coordinates'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('create index "geo_coordinates_spatialindex" on "geo" using gist ("coordinates")', $statements[0]); + } + + public function test_adding_fluent_spatial_index() + { + $blueprint = new Blueprint('geo'); + $blueprint->geometry('coordinates', 'point')->spatialIndex(); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(2, $statements); + $this->assertSame('create index "geo_coordinates_spatialindex" on "geo" using gist ("coordinates")', $statements[1]); + } + + public function test_adding_raw_index() + { + $blueprint = new Blueprint('users'); + $blueprint->rawIndex('(function(column))', 'raw_index'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('create index "raw_index" on "users" ((function(column)))', $statements[0]); + } + + public function test_adding_incrementing_id() + { + $blueprint = new Blueprint('users'); + $blueprint->increments('id'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "id" serial not null primary key', $statements[0]); + } + + public function test_adding_small_incrementing_id() + { + $blueprint = new Blueprint('users'); + $blueprint->smallIncrements('id'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "id" smallserial not null primary key', $statements[0]); + } + + public function test_adding_medium_incrementing_id() + { + $blueprint = new Blueprint('users'); + $blueprint->mediumIncrements('id'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "id" serial not null primary key', $statements[0]); + } + + public function test_adding_id() + { + $blueprint = new Blueprint('users'); + $blueprint->id(); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "id" bigserial not null primary key', $statements[0]); + + $blueprint = new Blueprint('users'); + $blueprint->id('foo'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "foo" bigserial not null primary key', $statements[0]); + } + + public function test_adding_foreign_id() + { + $blueprint = new Blueprint('users'); + $foreignId = $blueprint->foreignId('foo'); + $blueprint->foreignId('company_id')->constrained(); + $blueprint->foreignId('laravel_idea_id')->constrained(); + $blueprint->foreignId('team_id')->references('id')->on('teams'); + $blueprint->foreignId('team_column_id')->constrained('teams'); + + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertInstanceOf(ForeignIdColumnDefinition::class, $foreignId); + $this->assertSame([ + 'alter table "users" add column "foo" bigint not null, add column "company_id" bigint not null, add column "laravel_idea_id" bigint not null, add column "team_id" bigint not null, add column "team_column_id" bigint not null', + 'alter table "users" add constraint "users_company_id_foreign" foreign key ("company_id") references "companies" ("id")', + 'alter table "users" add constraint "users_laravel_idea_id_foreign" foreign key ("laravel_idea_id") references "laravel_ideas" ("id")', + 'alter table "users" add constraint "users_team_id_foreign" foreign key ("team_id") references "teams" ("id")', + 'alter table "users" add constraint "users_team_column_id_foreign" foreign key ("team_column_id") references "teams" ("id")', + ], $statements); + } + + public function test_adding_big_incrementing_id() + { + $blueprint = new Blueprint('users'); + $blueprint->bigIncrements('id'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "id" bigserial not null primary key', $statements[0]); + } + + public function test_adding_string() + { + $blueprint = new Blueprint('users'); + $blueprint->string('foo'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "foo" varchar(255) not null', $statements[0]); + + $blueprint = new Blueprint('users'); + $blueprint->string('foo', 100); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "foo" varchar(100) not null', $statements[0]); + + $blueprint = new Blueprint('users'); + $blueprint->string('foo', 100)->nullable()->default('bar'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "foo" varchar(100) null default \'bar\'', $statements[0]); + } + + public function test_adding_text() + { + $blueprint = new Blueprint('users'); + $blueprint->text('foo'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "foo" text not null', $statements[0]); + } + + public function test_adding_big_integer() + { + $blueprint = new Blueprint('users'); + $blueprint->bigInteger('foo'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "foo" bigint not null', $statements[0]); + + $blueprint = new Blueprint('users'); + $blueprint->bigInteger('foo', true); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "foo" bigserial not null primary key', $statements[0]); + } + + public function test_adding_integer() + { + $blueprint = new Blueprint('users'); + $blueprint->integer('foo'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "foo" integer not null', $statements[0]); + + $blueprint = new Blueprint('users'); + $blueprint->integer('foo', true); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "foo" serial not null primary key', $statements[0]); + } + + public function test_adding_medium_integer() + { + $blueprint = new Blueprint('users'); + $blueprint->mediumInteger('foo'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "foo" integer not null', $statements[0]); + + $blueprint = new Blueprint('users'); + $blueprint->mediumInteger('foo', true); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "foo" serial not null primary key', $statements[0]); + } + + public function test_adding_tiny_integer() + { + $blueprint = new Blueprint('users'); + $blueprint->tinyInteger('foo'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "foo" smallint not null', $statements[0]); + + $blueprint = new Blueprint('users'); + $blueprint->tinyInteger('foo', true); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "foo" smallserial not null primary key', $statements[0]); + } + + public function test_adding_small_integer() + { + $blueprint = new Blueprint('users'); + $blueprint->smallInteger('foo'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "foo" smallint not null', $statements[0]); + + $blueprint = new Blueprint('users'); + $blueprint->smallInteger('foo', true); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "foo" smallserial not null primary key', $statements[0]); + } + + public function test_adding_float() + { + $blueprint = new Blueprint('users'); + $blueprint->float('foo', 5); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "foo" float(5) not null', $statements[0]); + } + + public function test_adding_double() + { + $blueprint = new Blueprint('users'); + $blueprint->double('foo', 15, 8); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "foo" double precision not null', $statements[0]); + } + + public function test_adding_decimal() + { + $blueprint = new Blueprint('users'); + $blueprint->decimal('foo', 5, 2); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "foo" decimal(5, 2) not null', $statements[0]); + } + + public function test_adding_boolean() + { + $blueprint = new Blueprint('users'); + $blueprint->boolean('foo'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "foo" boolean not null', $statements[0]); + } + + public function test_adding_enum() + { + $blueprint = new Blueprint('users'); + $blueprint->enum('role', ['member', 'admin']); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "role" varchar(255) check ("role" in (\'member\', \'admin\')) not null', $statements[0]); + } + + public function test_adding_date() + { + $blueprint = new Blueprint('users'); + $blueprint->date('foo'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "foo" date not null', $statements[0]); + } + + public function test_adding_year() + { + $blueprint = new Blueprint('users'); + $blueprint->year('birth_year'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "birth_year" integer not null', $statements[0]); + } + + public function test_adding_json() + { + $blueprint = new Blueprint('users'); + $blueprint->json('foo'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "foo" json not null', $statements[0]); + } + + public function test_adding_jsonb() + { + $blueprint = new Blueprint('users'); + $blueprint->jsonb('foo'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "foo" jsonb not null', $statements[0]); + } + + public function test_adding_date_time() + { + $blueprint = new Blueprint('users'); + $blueprint->dateTime('created_at'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "created_at" timestamp(0) without time zone not null', $statements[0]); + } + + public function test_adding_date_time_with_precision() + { + $blueprint = new Blueprint('users'); + $blueprint->dateTime('created_at', 1); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "created_at" timestamp(1) without time zone not null', $statements[0]); + } + + public function test_adding_date_time_with_null_precision() + { + $blueprint = new Blueprint('users'); + $blueprint->dateTime('created_at', null); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "created_at" timestamp without time zone not null', $statements[0]); + } + + public function test_adding_date_time_tz() + { + $blueprint = new Blueprint('users'); + $blueprint->dateTimeTz('created_at'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "created_at" timestamp(0) with time zone not null', $statements[0]); + } + + public function test_adding_date_time_tz_with_precision() + { + $blueprint = new Blueprint('users'); + $blueprint->dateTimeTz('created_at', 1); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "created_at" timestamp(1) with time zone not null', $statements[0]); + } + + public function test_adding_date_time_tz_with_null_precision() + { + $blueprint = new Blueprint('users'); + $blueprint->dateTimeTz('created_at', null); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "created_at" timestamp with time zone not null', $statements[0]); + } + + public function test_adding_time() + { + $blueprint = new Blueprint('users'); + $blueprint->time('created_at'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "created_at" time(0) without time zone not null', $statements[0]); + } + + public function test_adding_time_with_precision() + { + $blueprint = new Blueprint('users'); + $blueprint->time('created_at', 1); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "created_at" time(1) without time zone not null', $statements[0]); + } + + public function test_adding_time_with_null_precision() + { + $blueprint = new Blueprint('users'); + $blueprint->time('created_at', null); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "created_at" time without time zone not null', $statements[0]); + } + + public function test_adding_time_tz() + { + $blueprint = new Blueprint('users'); + $blueprint->timeTz('created_at'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "created_at" time(0) with time zone not null', $statements[0]); + } + + public function test_adding_time_tz_with_precision() + { + $blueprint = new Blueprint('users'); + $blueprint->timeTz('created_at', 1); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "created_at" time(1) with time zone not null', $statements[0]); + } + + public function test_adding_time_tz_with_null_precision() + { + $blueprint = new Blueprint('users'); + $blueprint->timeTz('created_at', null); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "created_at" time with time zone not null', $statements[0]); + } + + public function test_adding_timestamp() + { + $blueprint = new Blueprint('users'); + $blueprint->timestamp('created_at'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "created_at" timestamp(0) without time zone not null', $statements[0]); + } + + public function test_adding_timestamp_with_precision() + { + $blueprint = new Blueprint('users'); + $blueprint->timestamp('created_at', 1); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "created_at" timestamp(1) without time zone not null', $statements[0]); + } + + public function test_adding_timestamp_with_null_precision() + { + $blueprint = new Blueprint('users'); + $blueprint->timestamp('created_at', null); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "created_at" timestamp without time zone not null', $statements[0]); + } + + public function test_adding_timestamp_tz() + { + $blueprint = new Blueprint('users'); + $blueprint->timestampTz('created_at'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "created_at" timestamp(0) with time zone not null', $statements[0]); + } + + public function test_adding_timestamp_tz_with_precision() + { + $blueprint = new Blueprint('users'); + $blueprint->timestampTz('created_at', 1); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "created_at" timestamp(1) with time zone not null', $statements[0]); + } + + public function test_adding_timestamp_tz_with_null_precision() + { + $blueprint = new Blueprint('users'); + $blueprint->timestampTz('created_at', null); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "created_at" timestamp with time zone not null', $statements[0]); + } + + public function test_adding_timestamps() + { + $blueprint = new Blueprint('users'); + $blueprint->timestamps(); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "created_at" timestamp(0) without time zone null, add column "updated_at" timestamp(0) without time zone null', $statements[0]); + } + + public function test_adding_timestamps_tz() + { + $blueprint = new Blueprint('users'); + $blueprint->timestampsTz(); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "created_at" timestamp(0) with time zone null, add column "updated_at" timestamp(0) with time zone null', $statements[0]); + } + + public function test_adding_binary() + { + $blueprint = new Blueprint('users'); + $blueprint->binary('foo'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "foo" bytea not null', $statements[0]); + } + + public function test_adding_uuid() + { + $blueprint = new Blueprint('users'); + $blueprint->uuid('foo'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "foo" uuid not null', $statements[0]); + } + + public function test_adding_foreign_uuid() + { + $blueprint = new Blueprint('users'); + $foreignUuid = $blueprint->foreignUuid('foo'); + $blueprint->foreignUuid('company_id')->constrained(); + $blueprint->foreignUuid('laravel_idea_id')->constrained(); + $blueprint->foreignUuid('team_id')->references('id')->on('teams'); + $blueprint->foreignUuid('team_column_id')->constrained('teams'); + + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertInstanceOf(ForeignIdColumnDefinition::class, $foreignUuid); + $this->assertSame([ + 'alter table "users" add column "foo" uuid not null, add column "company_id" uuid not null, add column "laravel_idea_id" uuid not null, add column "team_id" uuid not null, add column "team_column_id" uuid not null', + 'alter table "users" add constraint "users_company_id_foreign" foreign key ("company_id") references "companies" ("id")', + 'alter table "users" add constraint "users_laravel_idea_id_foreign" foreign key ("laravel_idea_id") references "laravel_ideas" ("id")', + 'alter table "users" add constraint "users_team_id_foreign" foreign key ("team_id") references "teams" ("id")', + 'alter table "users" add constraint "users_team_column_id_foreign" foreign key ("team_column_id") references "teams" ("id")', + ], $statements); + } + + /** + * @dataProvider generatedAsStatements + */ + public function test_adding_generated_as(callable $alter, string $expected) + { + $blueprint = new Blueprint('users'); + $alter($blueprint); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + $this->assertCount(1, $statements); + $this->assertSame($expected, $statements[0]); + } + + public function generatedAsStatements(): \Generator + { + yield 'default' => [ + fn (Blueprint $blueprint) => $blueprint->increments('foo')->generatedAs(), + 'alter table "users" add column "foo" integer not null generated by default as identity primary key', + ]; + + yield 'With always modifier' => [ + fn (Blueprint $blueprint) => $blueprint->increments('foo')->generatedAs()->always(), + 'alter table "users" add column "foo" integer not null generated always as identity primary key', + ]; + + yield 'With sequence options' => [ + fn (Blueprint $blueprint) => $blueprint->increments('foo')->generatedAs('increment by 10 start with 100'), + 'alter table "users" add column "foo" integer not null generated by default as identity (increment by 10 start with 100) primary key', + ]; + + yield 'Not a primary key' => [ + fn (Blueprint $blueprint) => $blueprint->integer('foo')->generatedAs(), + 'alter table "users" add column "foo" integer not null generated by default as identity', + ]; + } + + public function test_adding_virtual_as() + { + $blueprint = new Blueprint('users'); + $blueprint->integer('foo')->nullable(); + $blueprint->boolean('bar')->virtualAs('foo is not null'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "foo" integer null, add column "bar" boolean not null generated always as (foo is not null)', $statements[0]); + } + + public function test_adding_stored_as() + { + $blueprint = new Blueprint('users'); + $blueprint->integer('foo')->nullable(); + $blueprint->boolean('bar')->storedAs('foo is not null'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "foo" integer null, add column "bar" boolean not null generated always as (foo is not null) stored', $statements[0]); + } + + public function test_adding_ip_address() + { + $blueprint = new Blueprint('users'); + $blueprint->ipAddress('foo'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "foo" inet not null', $statements[0]); + } + + public function test_adding_mac_address() + { + $blueprint = new Blueprint('users'); + $blueprint->macAddress('foo'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "foo" macaddr not null', $statements[0]); + } + + public function test_compile_foreign() + { + $blueprint = new Blueprint('users'); + $blueprint->foreign('parent_id')->references('id')->on('parents')->onDelete('cascade')->deferrable(); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add constraint "users_parent_id_foreign" foreign key ("parent_id") references "parents" ("id") on delete cascade deferrable', $statements[0]); + + $blueprint = new Blueprint('users'); + $blueprint->foreign('parent_id')->references('id')->on('parents')->onDelete('cascade')->deferrable(false)->initiallyImmediate(); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add constraint "users_parent_id_foreign" foreign key ("parent_id") references "parents" ("id") on delete cascade not deferrable', $statements[0]); + + $blueprint = new Blueprint('users'); + $blueprint->foreign('parent_id')->references('id')->on('parents')->onDelete('cascade')->deferrable()->initiallyImmediate(false); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add constraint "users_parent_id_foreign" foreign key ("parent_id") references "parents" ("id") on delete cascade deferrable initially deferred', $statements[0]); + + $blueprint = new Blueprint('users'); + $blueprint->foreign('parent_id')->references('id')->on('parents')->onDelete('cascade')->deferrable()->notValid(); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add constraint "users_parent_id_foreign" foreign key ("parent_id") references "parents" ("id") on delete cascade deferrable not valid', $statements[0]); + } + + public function test_adding_geometry() + { + $blueprint = new Blueprint('geo'); + $blueprint->geometry('coordinates'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "geo" add column "coordinates" geometry not null', $statements[0]); + } + + public function test_adding_geography() + { + $blueprint = new Blueprint('geo'); + $blueprint->geography('coordinates', 'pointzm', 4269); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "geo" add column "coordinates" geography(pointzm,4269) not null', $statements[0]); + } + + public function test_adding_point() + { + $blueprint = new Blueprint('geo'); + $blueprint->geometry('coordinates', 'point'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "geo" add column "coordinates" geometry(point) not null', $statements[0]); + } + + public function test_adding_point_with_srid() + { + $blueprint = new Blueprint('geo'); + $blueprint->geometry('coordinates', 'point', 4269); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "geo" add column "coordinates" geometry(point,4269) not null', $statements[0]); + } + + public function test_adding_line_string() + { + $blueprint = new Blueprint('geo'); + $blueprint->geometry('coordinates', 'linestring'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "geo" add column "coordinates" geometry(linestring) not null', $statements[0]); + } + + public function test_adding_polygon() + { + $blueprint = new Blueprint('geo'); + $blueprint->geometry('coordinates', 'polygon'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "geo" add column "coordinates" geometry(polygon) not null', $statements[0]); + } + + public function test_adding_geometry_collection() + { + $blueprint = new Blueprint('geo'); + $blueprint->geometry('coordinates', 'geometrycollection'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "geo" add column "coordinates" geometry(geometrycollection) not null', $statements[0]); + } + + public function test_adding_multi_point() + { + $blueprint = new Blueprint('geo'); + $blueprint->geometry('coordinates', 'multipoint'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "geo" add column "coordinates" geometry(multipoint) not null', $statements[0]); + } + + public function test_adding_multi_line_string() + { + $blueprint = new Blueprint('geo'); + $blueprint->geometry('coordinates', 'multilinestring'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "geo" add column "coordinates" geometry(multilinestring) not null', $statements[0]); + } + + public function test_adding_multi_polygon() + { + $blueprint = new Blueprint('geo'); + $blueprint->geometry('coordinates', 'multipolygon'); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "geo" add column "coordinates" geometry(multipolygon) not null', $statements[0]); + } + + public function test_create_database() + { + $connection = $this->getConnection(); + $connection->shouldReceive('getConfig')->once()->once()->with('charset')->andReturn('utf8_foo'); + $statement = $this->getGrammar()->compileCreateDatabase('my_database_a', $connection); + + $this->assertSame( + 'create database "my_database_a" encoding "utf8_foo"', + $statement + ); + + $connection = $this->getConnection(); + $connection->shouldReceive('getConfig')->once()->once()->with('charset')->andReturn('utf8_bar'); + $statement = $this->getGrammar()->compileCreateDatabase('my_database_b', $connection); + + $this->assertSame( + 'create database "my_database_b" encoding "utf8_bar"', + $statement + ); + } + + public function test_drop_database_if_exists() + { + $statement = $this->getGrammar()->compileDropDatabaseIfExists('my_database_a'); + + $this->assertSame( + 'drop database if exists "my_database_a"', + $statement + ); + + $statement = $this->getGrammar()->compileDropDatabaseIfExists('my_database_b'); + + $this->assertSame( + 'drop database if exists "my_database_b"', + $statement + ); + } + + public function test_drop_all_tables_escapes_table_names() + { + $statement = $this->getGrammar()->compileDropAllTables(['alpha', 'beta', 'gamma']); + + $this->assertSame('drop table "alpha","beta","gamma" cascade', $statement); + } + + public function test_drop_all_views_escapes_table_names() + { + $statement = $this->getGrammar()->compileDropAllViews(['alpha', 'beta', 'gamma']); + + $this->assertSame('drop view "alpha","beta","gamma" cascade', $statement); + } + + public function test_drop_all_types_escapes_table_names() + { + $statement = $this->getGrammar()->compileDropAllTypes(['alpha', 'beta', 'gamma']); + + $this->assertSame('drop type "alpha","beta","gamma" cascade', $statement); + } + + public function test_creating_fulltext_indexes_throws_an_exception() + { + $this->expectException(FeatureNotSupportedException::class); + $blueprint = new Blueprint('fulltext'); + $fluent = new Fluent(); + $this->getGrammar()->compileFulltext($blueprint, $fluent); + } + + protected function getConnection() + { + return m::mock(Connection::class); + } + + public function getGrammar() + { + return new CockroachDbGrammar(); + } + + public function test_compile_columns() + { + $statement = $this->getGrammar()->compileColumns('public', 'table'); + + $this->assertStringContainsString("where c.relname = 'table' and n.nspname = 'public'", $statement); + } + + public function test_grammars_are_macroable() + { + // compileReplace macro. + $this->getGrammar()::macro('compileReplace', function () { + return true; + }); + + $c = $this->getGrammar()::compileReplace(); + + $this->assertTrue($c); + } +} diff --git a/tests/Database/Laravel9/DatabaseCockroachDbSchemaGrammarTest.php b/tests/Database/Laravel9/DatabaseCockroachDbSchemaGrammarTest.php index ae6b6cd..18a6a4e 100644 --- a/tests/Database/Laravel9/DatabaseCockroachDbSchemaGrammarTest.php +++ b/tests/Database/Laravel9/DatabaseCockroachDbSchemaGrammarTest.php @@ -9,7 +9,7 @@ use Mockery as m; use PHPUnit\Framework\TestCase; use YlsIdeas\CockroachDb\Exceptions\FeatureNotSupportedException; -use YlsIdeas\CockroachDb\Schema\CockroachGrammar; +use YlsIdeas\CockroachDb\Schema\CockroachDbGrammar; use YlsIdeas\CockroachDb\Tests\WithMultipleApplicationVersions; class DatabaseCockroachDbSchemaGrammarTest extends TestCase @@ -1059,7 +1059,7 @@ protected function getConnection() public function getGrammar() { - return new CockroachGrammar(); + return new CockroachDbGrammar(); } public function test_grammars_are_macroable() diff --git a/tests/Integration/Database/EloquentBelongsToManyTest.php b/tests/Integration/Database/EloquentBelongsToManyTest.php index cb71037..c36f841 100644 --- a/tests/Integration/Database/EloquentBelongsToManyTest.php +++ b/tests/Integration/Database/EloquentBelongsToManyTest.php @@ -728,13 +728,20 @@ public function test_can_touch_related_models() $post->tags()->touch(); foreach ($post->tags()->pluck('tags.updated_at') as $date) { - $this->assertSame('2017-10-10 10:10:10', $date); + $this->executeOnVersion('11.0.0', function () use ($date) { + $this->assertSame('2017-10-10 10:10:10', $date->toDateTimeString()); + }, function () use ($date) { + $this->assertSame('2017-10-10 10:10:10', $date); + }); } - $this->assertNotSame('2017-10-10 10:10:10', Tag::find(300)->updated_at); + $this->executeOnVersion('11.0.0', function () use ($date) { + $this->assertNotSame('2017-10-10 10:10:10', Tag::find(300)->updated_at?->toDateTimeString()); + }, function () use ($date) { + $this->assertNotSame('2017-10-10 10:10:10', Tag::find(300)->updated_at); + }); } - /** @group SkipMSSQL */ public function test_where_pivot_on_string() { $tag = Tag::create(['name' => Str::random()]); @@ -752,7 +759,6 @@ public function test_where_pivot_on_string() $this->assertEquals($relationTag->getAttributes(), $tag->getAttributes()); } - /** @group SkipMSSQL */ public function test_first_where() { $tag = Tag::create(['name' => 'foo']); @@ -770,7 +776,6 @@ public function test_first_where() $this->assertEquals($relationTag->getAttributes(), $tag->getAttributes()); } - /** @group SkipMSSQL */ public function test_where_pivot_on_boolean() { $tag = Tag::create(['name' => Str::random()]); @@ -788,7 +793,6 @@ public function test_where_pivot_on_boolean() $this->assertEquals($relationTag->getAttributes(), $tag->getAttributes()); } - /** @group SkipMSSQL */ public function test_where_pivot_in_method() { $tag = Tag::create(['name' => Str::random()]); @@ -824,7 +828,6 @@ public function test_or_where_pivot_in_method() $this->assertEquals($relationTags->pluck('id')->toArray(), [$tag1->id, $tag3->id]); } - /** @group SkipMSSQL */ public function test_where_pivot_not_in_method() { $tag1 = Tag::create(['name' => Str::random()]); @@ -864,7 +867,6 @@ public function test_or_where_pivot_not_in_method() $this->assertEquals($relationTags->pluck('id')->toArray(), [$tag1->id, $tag2->id]); } - /** @group SkipMSSQL */ public function test_where_pivot_null_method() { $tag1 = Tag::create(['name' => Str::random()]); @@ -883,7 +885,6 @@ public function test_where_pivot_null_method() $this->assertEquals($relationTag->getAttributes(), $tag2->getAttributes()); } - /** @group SkipMSSQL */ public function test_where_pivot_not_null_method() { $tag1 = Tag::create(['name' => Str::random()])->fresh(); @@ -1008,7 +1009,6 @@ public function test_pivot_doesnt_have_primary_key() $this->assertEquals(0, $user->postsWithCustomPivot()->first()->pivot->is_draft); } - /** @group SkipMSSQL */ public function test_order_by_pivot_method() { $tag1 = Tag::create(['name' => Str::random()]); diff --git a/tests/Integration/Database/EloquentCollectionLoadCountTest.php b/tests/Integration/Database/EloquentCollectionLoadCountTest.php index 5474b33..f3c741d 100644 --- a/tests/Integration/Database/EloquentCollectionLoadCountTest.php +++ b/tests/Integration/Database/EloquentCollectionLoadCountTest.php @@ -1,6 +1,6 @@ unsignedInteger('post_id'); }); - $post = Post::create(); - $post->comments()->saveMany([new Comment(), new Comment()]); + $post = PostLoadCollection::create(); + $post->comments()->saveMany([new CommentLoadCollection(), new CommentLoadCollection()]); - $post->likes()->save(new Like()); + $post->likes()->save(new LikeLoadCollection()); - Post::create(); + PostLoadCollection::create(); } public function test_load_count() { - $posts = Post::all(); + $posts = PostLoadCollection::all(); DB::enableQueryLog(); @@ -54,7 +53,7 @@ public function test_load_count() public function test_load_count_with_same_models() { - $posts = Post::all()->push(Post::first()); + $posts = PostLoadCollection::all()->push(PostLoadCollection::first()); DB::enableQueryLog(); @@ -68,7 +67,7 @@ public function test_load_count_with_same_models() public function test_load_count_on_deleted_models() { - $posts = Post::all()->each->delete(); + $posts = PostLoadCollection::all()->each->delete(); DB::enableQueryLog(); @@ -81,7 +80,7 @@ public function test_load_count_on_deleted_models() public function test_load_count_with_array_of_relations() { - $posts = Post::all(); + $posts = PostLoadCollection::all(); DB::enableQueryLog(); @@ -96,7 +95,7 @@ public function test_load_count_with_array_of_relations() public function test_load_count_does_not_override_attributes_with_default_value() { - $post = Post::first(); + $post = PostLoadCollection::first(); $post->some_default_value = 200; Collection::make([$post])->loadCount('comments'); @@ -106,9 +105,10 @@ public function test_load_count_does_not_override_attributes_with_default_value( } } -class Post extends Model +class PostLoadCollection extends Model { use SoftDeletes; + protected $table = 'posts'; protected $attributes = [ 'some_default_value' => 100, @@ -118,21 +118,23 @@ class Post extends Model public function comments() { - return $this->hasMany(Comment::class); + return $this->hasMany(CommentLoadCollection::class, 'post_id'); } public function likes() { - return $this->hasMany(Like::class); + return $this->hasMany(LikeLoadCollection::class, 'post_id'); } } -class Comment extends Model +class CommentLoadCollection extends Model { public $timestamps = false; + protected $table = 'comments'; } -class Like extends Model +class LikeLoadCollection extends Model { + protected $table = 'likes'; public $timestamps = false; } diff --git a/tests/Integration/Database/EloquentEagerLoadingLimitTest.php b/tests/Integration/Database/EloquentEagerLoadingLimitTest.php new file mode 100644 index 0000000..9645736 --- /dev/null +++ b/tests/Integration/Database/EloquentEagerLoadingLimitTest.php @@ -0,0 +1,195 @@ +skipIfOlderThan('11.0.0'); + } + + protected function afterRefreshingDatabase() + { + Schema::create('users', function (Blueprint $table) { + $table->id(); + }); + + Schema::create('posts', function (Blueprint $table) { + $table->id(); + $table->unsignedBigInteger('user_id'); + $table->timestamps(); + }); + + Schema::create('comments', function (Blueprint $table) { + $table->id(); + $table->unsignedBigInteger('post_id'); + $table->timestamps(); + }); + + Schema::create('roles', function (Blueprint $table) { + $table->id(); + $table->timestamps(); + }); + + Schema::create('role_user', function (Blueprint $table) { + $table->unsignedBigInteger('role_id'); + $table->unsignedBigInteger('user_id'); + }); + + EagerLoadLimitUser::create(['id' => 100]); + EagerLoadLimitUser::create(['id' => 200]); + + EagerLoadLimitPost::create(['id' => 100, 'user_id' => 100, 'created_at' => new Carbon('2024-01-01 00:00:01')]); + EagerLoadLimitPost::create(['id' => 200, 'user_id' => 100, 'created_at' => new Carbon('2024-01-01 00:00:02')]); + EagerLoadLimitPost::create(['id' => 300, 'user_id' => 100, 'created_at' => new Carbon('2024-01-01 00:00:03')]); + EagerLoadLimitPost::create(['id' => 400, 'user_id' => 200, 'created_at' => new Carbon('2024-01-01 00:00:04')]); + EagerLoadLimitPost::create(['id' => 500, 'user_id' => 200, 'created_at' => new Carbon('2024-01-01 00:00:05')]); + EagerLoadLimitPost::create(['id' => 600, 'user_id' => 200, 'created_at' => new Carbon('2024-01-01 00:00:06')]); + + EagerLoadLimitComment::create(['id' => 100, 'post_id' => 100, 'created_at' => new Carbon('2024-01-01 00:00:01')]); + EagerLoadLimitComment::create(['id' => 200, 'post_id' => 200, 'created_at' => new Carbon('2024-01-01 00:00:02')]); + EagerLoadLimitComment::create(['id' => 300, 'post_id' => 300, 'created_at' => new Carbon('2024-01-01 00:00:03')]); + EagerLoadLimitComment::create(['id' => 400, 'post_id' => 400, 'created_at' => new Carbon('2024-01-01 00:00:04')]); + EagerLoadLimitComment::create(['id' => 500, 'post_id' => 500, 'created_at' => new Carbon('2024-01-01 00:00:05')]); + EagerLoadLimitComment::create(['id' => 600, 'post_id' => 600, 'created_at' => new Carbon('2024-01-01 00:00:06')]); + + EagerLoadLimitRole::create(['id' => 100, 'created_at' => new Carbon('2024-01-01 00:00:01')]); + EagerLoadLimitRole::create(['id' => 200, 'created_at' => new Carbon('2024-01-01 00:00:02')]); + EagerLoadLimitRole::create(['id' => 300, 'created_at' => new Carbon('2024-01-01 00:00:03')]); + EagerLoadLimitRole::create(['id' => 400, 'created_at' => new Carbon('2024-01-01 00:00:04')]); + EagerLoadLimitRole::create(['id' => 500, 'created_at' => new Carbon('2024-01-01 00:00:05')]); + EagerLoadLimitRole::create(['id' => 600, 'created_at' => new Carbon('2024-01-01 00:00:06')]); + + DB::table('role_user')->insert([ + ['role_id' => 100, 'user_id' => 100], + ['role_id' => 200, 'user_id' => 100], + ['role_id' => 300, 'user_id' => 100], + ['role_id' => 400, 'user_id' => 200], + ['role_id' => 500, 'user_id' => 200], + ['role_id' => 600, 'user_id' => 200], + ]); + } + + public function test_belongs_to_many(): void + { + $users = EagerLoadLimitUser::with(['roles' => fn ($query) => $query->latest()->limit(2)]) + ->orderBy('id') + ->get(); + + $this->assertEquals([300, 200], $users[0]->roles->pluck('id')->all()); + $this->assertEquals([600, 500], $users[1]->roles->pluck('id')->all()); + $this->assertArrayNotHasKey('laravel_row', $users[0]->roles[0]); + $this->assertArrayNotHasKey('@laravel_group := `user_id`', $users[0]->roles[0]); + } + + public function test_belongs_to_many_with_offset(): void + { + $users = EagerLoadLimitUser::with(['roles' => fn ($query) => $query->latest()->limit(2)->offset(1)]) + ->orderBy('id') + ->get(); + + $this->assertEquals([200, 100], $users[0]->roles->pluck('id')->all()); + $this->assertEquals([500, 400], $users[1]->roles->pluck('id')->all()); + } + + public function test_has_many(): void + { + $users = EagerLoadLimitUser::with(['posts' => fn ($query) => $query->latest()->limit(2)]) + ->orderBy('id') + ->get(); + + $this->assertEquals([300, 200], $users[0]->posts->pluck('id')->all()); + $this->assertEquals([600, 500], $users[1]->posts->pluck('id')->all()); + $this->assertArrayNotHasKey('laravel_row', $users[0]->posts[0]); + $this->assertArrayNotHasKey('@laravel_group := `user_id`', $users[0]->posts[0]); + } + + public function test_has_many_with_offset(): void + { + $users = EagerLoadLimitUser::with(['posts' => fn ($query) => $query->latest()->limit(2)->offset(1)]) + ->orderBy('id') + ->get(); + + $this->assertEquals([200, 100], $users[0]->posts->pluck('id')->all()); + $this->assertEquals([500, 400], $users[1]->posts->pluck('id')->all()); + } + + public function test_has_many_through(): void + { + $users = EagerLoadLimitUser::with(['comments' => fn ($query) => $query->latest('comments.created_at')->limit(2)]) + ->orderBy('id') + ->get(); + + $this->assertEquals([300, 200], $users[0]->comments->pluck('id')->all()); + $this->assertEquals([600, 500], $users[1]->comments->pluck('id')->all()); + $this->assertArrayNotHasKey('laravel_row', $users[0]->comments[0]); + $this->assertArrayNotHasKey('@laravel_group := `user_id`', $users[0]->comments[0]); + } + + public function test_has_many_through_with_offset(): void + { + $users = EagerLoadLimitUser::with(['comments' => fn ($query) => $query->latest('comments.created_at')->limit(2)->offset(1)]) + ->orderBy('id') + ->get(); + + $this->assertEquals([200, 100], $users[0]->comments->pluck('id')->all()); + $this->assertEquals([500, 400], $users[1]->comments->pluck('id')->all()); + } +} + +class EagerLoadLimitComment extends Model +{ + protected $table = 'comments'; + public $timestamps = false; + + protected $guarded = []; +} + +class EagerLoadLimitPost extends Model +{ + protected $table = 'posts'; + protected $guarded = []; +} + +class EagerLoadLimitRole extends Model +{ + protected $table = 'roles'; + protected $guarded = []; +} + +class EagerLoadLimitUser extends Model +{ + protected $table = 'users'; + public $timestamps = false; + + protected $guarded = []; + + public function comments(): HasManyThrough + { + return $this->hasManyThrough(EagerLoadLimitComment::class, EagerLoadLimitPost::class, 'user_id', 'post_id'); + } + + public function posts(): HasMany + { + return $this->hasMany(EagerLoadLimitPost::class, 'user_id'); + } + + public function roles(): BelongsToMany + { + return $this->belongsToMany(EagerLoadLimitRole::class, 'role_user', 'user_id', 'role_id'); + } +} diff --git a/tests/Integration/Database/EloquentModelCustomCastingTest.php b/tests/Integration/Database/EloquentModelCustomCastingTest.php new file mode 100644 index 0000000..603b2d1 --- /dev/null +++ b/tests/Integration/Database/EloquentModelCustomCastingTest.php @@ -0,0 +1,418 @@ +skipIfOlderThan('9.28.0'); + $db = new DB(); + + $db->addConnection([ + 'driver' => 'sqlite', + 'database' => ':memory:', + ]); + + $db->bootEloquent(); + $db->setAsGlobal(); + + $this->createSchema(); + } + + /** + * Setup the database schema. + * + * @return void + */ + public function createSchema() + { + $this->schema()->create('casting_table', function (Blueprint $table) { + $table->increments('id'); + $table->string('address_line_one'); + $table->string('address_line_two'); + $table->integer('amount'); + $table->string('string_field'); + $table->timestamps(); + }); + + $this->schema()->create('members', function (Blueprint $table) { + $table->increments('id'); + $table->decimal('amount', 4, 2); + }); + } + + /** + * Tear down the database schema. + * + * @return void + */ + protected function tearDown(): void + { + $this->schema()->drop('casting_table'); + $this->schema()->drop('members'); + } + + #[RequiresPhpExtension('gmp')] + public function test_saving_casted_attributes_to_database() + { + /** @var \YlsIdeas\CockroachDb\Tests\Integration\Database\CustomCasts $model */ + $model = CustomCasts::create([ + 'address' => new AddressModel('address_line_one_value', 'address_line_two_value'), + 'amount' => gmp_init('1000', 10), + 'string_field' => null, + ]); + + $this->assertSame('address_line_one_value', $model->getOriginal('address_line_one')); + $this->assertSame('address_line_one_value', $model->getAttribute('address_line_one')); + + $this->assertSame('address_line_two_value', $model->getOriginal('address_line_two')); + $this->assertSame('address_line_two_value', $model->getAttribute('address_line_two')); + + $this->assertSame('1000', $model->getRawOriginal('amount')); + + $this->assertNull($model->getOriginal('string_field')); + $this->assertNull($model->getAttribute('string_field')); + $this->assertSame('', $model->getRawOriginal('string_field')); + + /** @var \YlsIdeas\CockroachDb\Tests\Integration\Database\CustomCasts $another_model */ + $another_model = CustomCasts::create([ + 'address_line_one' => 'address_line_one_value', + 'address_line_two' => 'address_line_two_value', + 'amount' => gmp_init('500', 10), + 'string_field' => 'string_value', + ]); + + $this->assertInstanceOf(AddressModel::class, $another_model->address); + + $this->assertSame('address_line_one_value', $model->address->lineOne); + $this->assertSame('address_line_two_value', $model->address->lineTwo); + $this->assertInstanceOf(GMP::class, $model->amount); + } + + #[RequiresPhpExtension('gmp')] + public function test_invalid_argument_exception_on_invalid_value() + { + /** @var \YlsIdeas\CockroachDb\Tests\Integration\Database\CustomCasts $model */ + $model = CustomCasts::create([ + 'address' => new AddressModel('address_line_one_value', 'address_line_two_value'), + 'amount' => gmp_init('1000', 10), + 'string_field' => 'string_value', + ]); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The given value is not an Address instance.'); + $model->address = 'single_string'; + + // Ensure model values remain unchanged + $this->assertSame('address_line_one_value', $model->address->lineOne); + $this->assertSame('address_line_two_value', $model->address->lineTwo); + } + + #[RequiresPhpExtension('gmp')] + public function test_invalid_argument_exception_on_null() + { + /** @var \YlsIdeas\CockroachDb\Tests\Integration\Database\CustomCasts $model */ + $model = CustomCasts::create([ + 'address' => new AddressModel('address_line_one_value', 'address_line_two_value'), + 'amount' => gmp_init('1000', 10), + 'string_field' => 'string_value', + ]); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The given value is not an Address instance.'); + $model->address = null; + + // Ensure model values remain unchanged + $this->assertSame('address_line_one_value', $model->address->lineOne); + $this->assertSame('address_line_two_value', $model->address->lineTwo); + } + + #[RequiresPhpExtension('gmp')] + public function test_models_with_custom_casts_can_be_converted_to_arrays() + { + /** @var \YlsIdeas\CockroachDb\Tests\Integration\Database\CustomCasts $model */ + $model = CustomCasts::create([ + 'address' => new AddressModel('address_line_one_value', 'address_line_two_value'), + 'amount' => gmp_init('1000', 10), + 'string_field' => 'string_value', + ]); + + // Ensure model values remain unchanged + $this->assertSame([ + 'address_line_one' => 'address_line_one_value', + 'address_line_two' => 'address_line_two_value', + 'amount' => '1000', + 'string_field' => 'string_value', + 'updated_at' => $model->updated_at->toJSON(), + 'created_at' => $model->created_at->toJSON(), + 'id' => 1, + ], $model->toArray()); + } + + public function test_model_with_custom_casts_work_with_custom_increment_decrement() + { + $this->skipIfOlderThan('11.0.0'); + + $model = new Member(); + $model->amount = new Euro('2'); + $model->save(); + + $this->assertInstanceOf(Euro::class, $model->amount); + $this->assertEquals('2', $model->amount->value); + + $model->increment('amount', new Euro('1')); + $this->assertEquals('3.00', $model->amount->value); + } + + /** + * Get a database connection instance. + * + * @return \Illuminate\Database\Connection + */ + protected function connection() + { + return Eloquent::getConnectionResolver()->connection(); + } + + /** + * Get a schema builder instance. + * + * @return \Illuminate\Database\Schema\Builder + */ + protected function schema() + { + return $this->connection()->getSchemaBuilder(); + } +} + +/** + * Eloquent Casts... + */ +class AddressCast implements CastsAttributes +{ + /** + * Cast the given value. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @param string $key + * @param mixed $value + * @param array $attributes + * @return \Illuminate\Tests\Integration\Database\AddressModel + */ + public function get($model, $key, $value, $attributes) + { + return new AddressModel( + $attributes['address_line_one'], + $attributes['address_line_two'], + ); + } + + /** + * Prepare the given value for storage. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @param string $key + * @param AddressModel $value + * @param array $attributes + * @return array + */ + public function set($model, $key, $value, $attributes) + { + if (! $value instanceof AddressModel) { + throw new InvalidArgumentException('The given value is not an Address instance.'); + } + + return [ + 'address_line_one' => $value->lineOne, + 'address_line_two' => $value->lineTwo, + ]; + } +} + +class GMPCast implements CastsAttributes, SerializesCastableAttributes +{ + /** + * Cast the given value. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @param string $key + * @param string $value + * @param array $attributes + * @return string|null + */ + public function get($model, $key, $value, $attributes) + { + return gmp_init($value, 10); + } + + /** + * Prepare the given value for storage. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @param string $key + * @param string|null $value + * @param array $attributes + * @return string + */ + public function set($model, $key, $value, $attributes) + { + return gmp_strval($value, 10); + } + + /** + * Serialize the attribute when converting the model to an array. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @param string $key + * @param mixed $value + * @param array $attributes + * @return mixed + */ + public function serialize($model, string $key, $value, array $attributes) + { + return gmp_strval($value, 10); + } +} + +class NonNullableString implements CastsAttributes +{ + /** + * Cast the given value. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @param string $key + * @param string $value + * @param array $attributes + * @return string|null + */ + public function get($model, $key, $value, $attributes) + { + return ($value != '') ? $value : null; + } + + /** + * Prepare the given value for storage. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @param string $key + * @param string|null $value + * @param array $attributes + * @return string + */ + public function set($model, $key, $value, $attributes) + { + return $value ?? ''; + } +} + +/** + * Eloquent Models... + */ +class CustomCasts extends Eloquent +{ + /** + * @var string + */ + protected $table = 'casting_table'; + + /** + * @var string[] + */ + protected $guarded = []; + + /** + * @var array + */ + protected $casts = [ + 'address' => AddressCast::class, + 'amount' => GMPCast::class, + 'string_field' => NonNullableString::class, + ]; +} + +class AddressModel +{ + /** + * @var string + */ + public $lineOne; + + /** + * @var string + */ + public $lineTwo; + + public function __construct($address_line_one, $address_line_two) + { + $this->lineOne = $address_line_one; + $this->lineTwo = $address_line_two; + } +} + +class Euro implements Castable +{ + public string $value; + + public function __construct(string $value) + { + $this->value = $value; + } + + public static function castUsing(array $arguments) + { + return EuroCaster::class; + } +} + +class EuroCaster implements CastsAttributes +{ + public function get($model, $key, $value, $attributes) + { + return new Euro($value); + } + + public function set($model, $key, $value, $attributes) + { + return $value instanceof Euro ? $value->value : $value; + } + + public function increment($model, $key, $value, $attributes) + { + $model->$key = new Euro((string) BigNumber::of($model->$key->value)->plus($value->value)->toScale(2)); + + return $model->$key; + } + + public function decrement($model, $key, $value, $attributes) + { + $model->$key = new Euro((string) BigNumber::of($model->$key->value)->subtract($value->value)->toScale(2)); + + return $model->$key; + } +} + +class Member extends Model +{ + public $timestamps = false; + protected $casts = [ + 'amount' => Euro::class, + ]; +} diff --git a/tests/Integration/Database/Fixtures/TinyInteger.php b/tests/Integration/Database/Fixtures/TinyInteger.php deleted file mode 100644 index fda2810..0000000 --- a/tests/Integration/Database/Fixtures/TinyInteger.php +++ /dev/null @@ -1,38 +0,0 @@ -