From 9b6e3772dc018b71b1697e5f25e78647b1033c7f Mon Sep 17 00:00:00 2001 From: Laravel Freelancer NL <36150929+LaravelFreelancerNL@users.noreply.github.com> Date: Sun, 18 Aug 2024 12:48:21 +0200 Subject: [PATCH 01/34] Update readme.md --- readme.md | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/readme.md b/readme.md index 5a80090..8af3f21 100644 --- a/readme.md +++ b/readme.md @@ -18,20 +18,19 @@ The goal is to create a drop-in ArangoDB replacement for Laravel's database, mig **This package is in development; use at your own peril.** ## Installation -You may use composer to install Aranguent: +This driver is currently in the v1 beta stage. +To install it make sure that the minimum stability is +set to beta or lower, and that prefer-stable is set to false in composer.json: -``` composer require laravel-freelancer-nl/aranguent ``` +``` + "minimum-stability": "beta", + "prefer-stable": false, +``` -While this driver is in the beta stage, changes are you will get a type error upon installation. +You may then use composer to install Aranguent: -composer.json will probably not list a specific version: -```"laravel-freelancer-nl/aranguent": "*"``` +``` composer require laravel-freelancer-nl/aranguent ``` -If so, either set the minimum-stability level to 'dev' or install the latest version: -``` -composer require laravel-freelancer-nl/aranguent:v1.0.0-beta.8 laravel-freelancer-nl/fluentaql:2.1.1 -``` -This updates the package to the latest beta, and properly installs the fluentaql package as well. ### Version compatibility From 94fdcde56701549ee4930ceb68c40eddcbe5de4d Mon Sep 17 00:00:00 2001 From: Laravel Freelancer NL <36150929+LaravelFreelancerNL@users.noreply.github.com> Date: Sun, 25 Aug 2024 13:50:32 +0200 Subject: [PATCH 02/34] Fix/eager loading fail (#160) * Fixed relationship getKey key sorting * Removed old db assertion overrides (QB improvements renders these overrides unnecessary) --- TestSetup/Database/Seeders/HousesSeeder.php | 3 + .../Concerns/IsAranguentRelation.php | 22 ++++ .../Concerns/InteractsWithDatabase.php | 114 ------------------ tests/Eloquent/BelongsToManyTest.php | 30 ++++- tests/Eloquent/BelongsToTest.php | 27 +++++ tests/Eloquent/HasManyTest.php | 8 ++ tests/Query/WheresTest.php | 33 +++-- tests/Testing/InteractsWithDatabaseTest.php | 1 + 8 files changed, 110 insertions(+), 128 deletions(-) diff --git a/TestSetup/Database/Seeders/HousesSeeder.php b/TestSetup/Database/Seeders/HousesSeeder.php index ac93449..abc7bf5 100644 --- a/TestSetup/Database/Seeders/HousesSeeder.php +++ b/TestSetup/Database/Seeders/HousesSeeder.php @@ -19,6 +19,7 @@ public function run() "_key": "lannister", "name": "Lannister", "location_id": "king-s-landing", + "led_by": "TywinLannister", "en": { "description": "House Lannister of Casterly Rock is one of the Great Houses of Westeros, one of its richest and most powerful families and one of its oldest dynasties. It was also the royal House of the Seven Kingdoms following the extinction of House Baratheon of King\'s Landing, which had been their puppet House during the War of the Five Kings, for a brief stint of time until House Targaryen took back the Iron Throne in Daenerys Targaryen\'s war for Westeros." } @@ -27,6 +28,7 @@ public function run() "_key": "stark", "name": "Stark", "location_id": "winterfell", + "led_by": "NedStark", "en": { "description": "House Stark of Winterfell is a Great House of Westeros and the royal house of the Kingdom of the North. They rule over the vast region known as the North from their seat in Winterfell. It is one of the oldest lines of Westerosi nobility by far, claiming a line of descent stretching back over eight thousand years. Before the Targaryen conquest, as well as during the War of the Five Kings and early on in Daenerys Targaryen\'s war for Westeros, the leaders of House Stark ruled over the region as the Kings in the North." } @@ -34,6 +36,7 @@ public function run() { "_key": "targaryen", "name": "Targaryen", + "led_by": "DaenerysTargaryen", "location_id": "dragonstone", "en": { "coat-of-arms": "A three-headed dragon breathing flames, red on black (Sable, a dragon thrice-headed gules)", diff --git a/src/Eloquent/Relations/Concerns/IsAranguentRelation.php b/src/Eloquent/Relations/Concerns/IsAranguentRelation.php index d0b0537..228b322 100644 --- a/src/Eloquent/Relations/Concerns/IsAranguentRelation.php +++ b/src/Eloquent/Relations/Concerns/IsAranguentRelation.php @@ -9,6 +9,28 @@ trait IsAranguentRelation { + /** + * Get all of the primary keys for an array of models. + * + * @param array $models + * @param string|null $key + * @return array + */ + protected function getKeys(array $models, $key = null) + { + // The original function orders the results associatively by value which means the keys reorder too. + // However, a list of keys with unordered numeric keys will be recognized as an object down the line + // for json casting while we need a list of keys. + + $keys = collect($models)->map(function ($value) use ($key) { + return $key ? $value->getAttribute($key) : $value->getKey(); + })->values()->unique(null, true)->all(); + + sort($keys); + + return $keys; + } + /** * Add the constraints for a relationship count query. * diff --git a/src/Testing/Concerns/InteractsWithDatabase.php b/src/Testing/Concerns/InteractsWithDatabase.php index f73e651..2da9780 100644 --- a/src/Testing/Concerns/InteractsWithDatabase.php +++ b/src/Testing/Concerns/InteractsWithDatabase.php @@ -5,122 +5,9 @@ namespace LaravelFreelancerNL\Aranguent\Testing\Concerns; use Illuminate\Support\Facades\DB; -use Illuminate\Testing\Constraints\HasInDatabase; -use Illuminate\Testing\Constraints\NotSoftDeletedInDatabase; -use Illuminate\Testing\Constraints\SoftDeletedInDatabase; -use PHPUnit\Framework\Constraint\LogicalNot as ReverseConstraint; trait InteractsWithDatabase { - /** - * Assert that a given where condition exists in the database. - * - * @param \Illuminate\Database\Eloquent\Model|string $table - * @param array $data - * @param string|null $connection - * @return $this - */ - protected function assertDatabaseHas($table, array $data, $connection = null) - { - $this->assertThat( - $this->getTable($table), - new HasInDatabase($this->getConnection($connection), associativeFlatten($data)), - ); - - return $this; - } - - /** - * Assert that a given where condition does not exist in the database. - * - * @param \Illuminate\Database\Eloquent\Model|string $table - * @param array $data - * @param string|null $connection - * @return $this - */ - protected function assertDatabaseMissing($table, array $data, $connection = null) - { - $constraint = new ReverseConstraint( - new HasInDatabase($this->getConnection($connection), associativeFlatten($data)), - ); - - $this->assertThat($this->getTable($table), $constraint); - - return $this; - } - - /** - * Assert the given record has been "soft deleted". - * - * @param \Illuminate\Database\Eloquent\Model|string $table - * @param array $data - * @param string|null $connection - * @param string|null $deletedAtColumn - * @return $this - */ - protected function assertSoftDeleted( - $table, - array $data = [], - $connection = null, - $deletedAtColumn = 'deleted_at', - ) { - if ($this->isSoftDeletableModel($table) && !is_string($table)) { - return $this->assertSoftDeleted( - $table->getTable(), - [$table->getKeyName() => $table->getKey()], - $table->getConnectionName(), /** @phpstan-ignore-next-line */ - $table->getDeletedAtColumn(), - ); - } - - $this->assertThat( - $this->getTable($table), - new SoftDeletedInDatabase( - $this->getConnection($connection), - associativeFlatten($data), - (string) $deletedAtColumn, - ), - ); - - return $this; - } - - /** - * Assert the given record has not been "soft deleted". - * - * @param \Illuminate\Database\Eloquent\Model|string $table - * @param array $data - * @param string|null $connection - * @param string|null $deletedAtColumn - * @return $this - */ - protected function assertNotSoftDeleted( - $table, - array $data = [], - $connection = null, - $deletedAtColumn = 'deleted_at', - ) { - if ($this->isSoftDeletableModel($table) && !is_string($table)) { - return $this->assertNotSoftDeleted( - $table->getTable(), - [$table->getKeyName() => $table->getKey()], - $table->getConnectionName(), /** @phpstan-ignore-next-line */ - $table->getDeletedAtColumn(), - ); - } - - $this->assertThat( - $this->getTable($table), - new NotSoftDeletedInDatabase( - $this->getConnection($connection), - associativeFlatten($data), - (string) $deletedAtColumn, - ), - ); - - return $this; - } - /** * Cast a JSON string to a database compatible type. * Supported for backwards compatibility in existing projects. @@ -133,5 +20,4 @@ public function castAsJson($value) { return DB::raw($value); } - } diff --git a/tests/Eloquent/BelongsToManyTest.php b/tests/Eloquent/BelongsToManyTest.php index 8692420..cff60db 100644 --- a/tests/Eloquent/BelongsToManyTest.php +++ b/tests/Eloquent/BelongsToManyTest.php @@ -51,7 +51,7 @@ test('attach', function () { $child = Character::find('JonSnow'); - $lyannaStark = Character::firstOrCreate( + Character::firstOrCreate( [ 'id' => 'LyannaStark', 'name' => 'Lyanna', @@ -68,7 +68,7 @@ $child->parents()->attach($lyannaStark); $child->save(); - $reloadedChild = Character::find('JonSnow'); + Character::find('JonSnow'); $parents = $child->parents; expect($parents[0]->id)->toEqual('NedStark'); @@ -157,3 +157,29 @@ expect($char->tags[1]->pivot->tag_id)->toBeString(); expect($char->tags[1]->pivot->tag_id)->toBe('2'); }); + +test('with', function () { + $parent = Character::with('children')->find('NedStark'); + + expect($parent->children)->toHaveCount(5); + expect($parent->children->first()->id)->toEqual('RobbStark'); +}); + +test('with on multiple models', function () { + $characters = Character::with('children')->where('surname', 'Stark')->get(); + + expect($characters)->toHaveCount(6); + expect($characters[0]->id)->toEqual('NedStark'); + expect($characters[0]->children->first()->id)->toEqual('AryaStark'); + expect($characters[0]->children)->toHaveCount(5); + expect($characters[1]->id)->toEqual('CatelynStark'); + expect($characters[1]->children)->toHaveCount(4); +}); + +test('load', function () { + $parent = Character::find('NedStark'); + $parent->load('children'); + + expect($parent->children)->toHaveCount(5); + expect($parent->children->first()->id)->toEqual('RobbStark'); +}); diff --git a/tests/Eloquent/BelongsToTest.php b/tests/Eloquent/BelongsToTest.php index 843498e..65dfdee 100644 --- a/tests/Eloquent/BelongsToTest.php +++ b/tests/Eloquent/BelongsToTest.php @@ -5,6 +5,7 @@ use LaravelFreelancerNL\Aranguent\Testing\DatabaseTransactions; use Mockery as M; use TestSetup\Models\Character; +use TestSetup\Models\House; use TestSetup\Models\Location; uses( @@ -82,3 +83,29 @@ expect($location->leader)->toBeInstanceOf(Character::class); expect($location->leader->id)->toEqual('SansaStark'); }); + + +test('with on single model', function () { + $house = House::with('head')->find('lannister'); + + expect($house->head)->toBeInstanceOf(Character::class); + expect($house->head->id)->toEqual('TywinLannister'); +}); + + +test('with on multiple model', function () { + $houses = House::with('head')->get(); + + expect($houses->count())->toBe(3); + expect($houses->first()->head)->toBeInstanceOf(Character::class); + expect($houses->first()->head->id)->toEqual('TywinLannister'); +}); + + +test('load', function () { + $house = House::find('lannister'); + $house->load('head'); + + expect($house->head)->toBeInstanceOf(Character::class); + expect($house->head->id)->toEqual('TywinLannister'); +}); diff --git a/tests/Eloquent/HasManyTest.php b/tests/Eloquent/HasManyTest.php index 109621a..23d7698 100644 --- a/tests/Eloquent/HasManyTest.php +++ b/tests/Eloquent/HasManyTest.php @@ -89,3 +89,11 @@ expect($location->inhabitants->first())->toBeInstanceOf(Character::class); expect($location->inhabitants->first()->id)->toEqual('NedStark'); }); + +test('with on multiple models', function () { + $locations = Location::with('inhabitants')->get(); + + expect($locations->first()->inhabitants->first())->toBeInstanceOf(Character::class); + expect($locations->first()->inhabitants)->toHaveCount(2); + expect($locations[8]->inhabitants)->toHaveCount(0); +}); diff --git a/tests/Query/WheresTest.php b/tests/Query/WheresTest.php index 3e0ece1..bd5da57 100644 --- a/tests/Query/WheresTest.php +++ b/tests/Query/WheresTest.php @@ -229,20 +229,29 @@ ); }); -test('where in', function () { - $builder = getBuilder(); +test('whereIn', function () { + $results = DB::table('characters') + ->whereIn( + 'characters.residence_id', + [ + "astapor", + "beyond-the-wall", + "dragonstone", + "king-s-landing", + "riverrun", + "the-red-keep", + "vaes-dothrak", + "winterfell", + "yunkai", + ], + )->get(); + + expect($results)->toHaveCount(33); + expect($results->first()->_id)->toBe('characters/NedStark'); + expect($results->first()->residence_id)->toBe('winterfell'); +}); - $builder->select() - ->from('users') - ->whereIn('country', ['The Netherlands', 'Germany', 'Great-Britain']); - $this->assertSame( - 'FOR userDoc IN users FILTER `userDoc`.`country` IN @' - . $builder->getQueryId() - . '_where_1 RETURN userDoc', - $builder->toSql(), - ); -}); test('where integer in raw', function () { $builder = getBuilder(); diff --git a/tests/Testing/InteractsWithDatabaseTest.php b/tests/Testing/InteractsWithDatabaseTest.php index a91294f..d32459a 100644 --- a/tests/Testing/InteractsWithDatabaseTest.php +++ b/tests/Testing/InteractsWithDatabaseTest.php @@ -72,6 +72,7 @@ test('assert model exists', function () { $ned = Character::find('NedStark'); + $this->assertModelExists($ned); }); From b3f9b158e64d7003ed02b0a4aca2b1f237cfc9d4 Mon Sep 17 00:00:00 2001 From: Laravel Freelancer NL <36150929+LaravelFreelancerNL@users.noreply.github.com> Date: Wed, 18 Sep 2024 10:56:54 +0200 Subject: [PATCH 03/34] Fixed incorrect query duration logging (#161) * Fix incorrect query duration * Fix styling * Added migration convert for clean installs/dep updates * Ignore false positive * Improved preg_match handling * Loosened exact error message matching to deal with us probably a version differences --------- Co-authored-by: LaravelFreelancerNL --- .../2019_11_15_000000_create_characters_table.php | 2 +- .../2019_11_15_000000_create_children_edge_table.php | 2 +- .../2019_11_15_000000_create_houses_table.php | 2 +- .../2019_11_15_000000_create_locations_table.php | 2 +- .../2019_11_15_000000_create_taggables_table.php | 2 +- .../2019_11_15_000000_create_tags_table.php | 2 +- .../2021_11_22_145621_create_house_view.php | 2 +- ...4_01_04_145621_create_house_search_alias_view.php | 2 +- .../2024_08_05_145621_create_events_table.php | 2 +- bin/qa.sh | 1 + src/Concerns/RunsQueries.php | 2 +- src/Connection.php | 4 ++-- src/Eloquent/Casts/AsArrayObject.php | 2 +- src/Eloquent/Concerns/IsAranguentModel.php | 4 +++- src/Providers/MigrationServiceProvider.php | 2 +- src/Query/Concerns/BuildsGroups.php | 2 +- src/Query/Concerns/BuildsSearches.php | 2 +- src/Query/Concerns/BuildsUpdates.php | 6 +++--- src/Query/Concerns/BuildsWheres.php | 2 +- src/Query/Concerns/CompilesColumns.php | 2 +- src/Query/Concerns/CompilesDataManipulations.php | 10 +++++----- src/Query/Concerns/CompilesGroups.php | 2 +- src/Query/Concerns/CompilesJoins.php | 12 +++++++----- src/Query/Concerns/ConvertsIdToKey.php | 2 +- src/Query/Concerns/HandlesAliases.php | 11 ++++++----- src/Query/Concerns/HandlesAqlGrammar.php | 12 ++++++------ src/Query/Grammar.php | 2 +- src/Testing/DatabaseMigrations.php | 2 +- src/Testing/DatabaseTruncation.php | 2 +- src/Testing/RefreshDatabase.php | 2 +- src/helpers.php | 2 +- tests/Console/ModelMakeCommandTest.php | 4 ++-- tests/Migrations/MigrationRepositoryTest.php | 4 ++-- tests/Query/DataRetrievalTest.php | 2 +- tests/Query/DebugTest.php | 4 ++-- tests/QueryExceptionTest.php | 8 +------- tests/Schema/SchemaBuilderTest.php | 2 +- tests/Schema/TableTest.php | 2 +- tests/TestCase.php | 2 +- 39 files changed, 67 insertions(+), 67 deletions(-) diff --git a/TestSetup/Database/Migrations/2019_11_15_000000_create_characters_table.php b/TestSetup/Database/Migrations/2019_11_15_000000_create_characters_table.php index 986926f..64dd563 100644 --- a/TestSetup/Database/Migrations/2019_11_15_000000_create_characters_table.php +++ b/TestSetup/Database/Migrations/2019_11_15_000000_create_characters_table.php @@ -6,7 +6,7 @@ use LaravelFreelancerNL\Aranguent\Facades\Schema; use LaravelFreelancerNL\Aranguent\Schema\Blueprint; -return new class () extends Migration { +return new class extends Migration { /** * Run the migrations. * diff --git a/TestSetup/Database/Migrations/2019_11_15_000000_create_children_edge_table.php b/TestSetup/Database/Migrations/2019_11_15_000000_create_children_edge_table.php index 21c7b48..1adefa1 100644 --- a/TestSetup/Database/Migrations/2019_11_15_000000_create_children_edge_table.php +++ b/TestSetup/Database/Migrations/2019_11_15_000000_create_children_edge_table.php @@ -6,7 +6,7 @@ use LaravelFreelancerNL\Aranguent\Facades\Schema; use LaravelFreelancerNL\Aranguent\Schema\Blueprint; -return new class () extends Migration { +return new class extends Migration { public const EDGE_COLLECTION = 3; /** diff --git a/TestSetup/Database/Migrations/2019_11_15_000000_create_houses_table.php b/TestSetup/Database/Migrations/2019_11_15_000000_create_houses_table.php index 21c9949..020c698 100644 --- a/TestSetup/Database/Migrations/2019_11_15_000000_create_houses_table.php +++ b/TestSetup/Database/Migrations/2019_11_15_000000_create_houses_table.php @@ -6,7 +6,7 @@ use LaravelFreelancerNL\Aranguent\Facades\Schema; use LaravelFreelancerNL\Aranguent\Schema\Blueprint; -return new class () extends Migration { +return new class extends Migration { /** * Run the migrations. * diff --git a/TestSetup/Database/Migrations/2019_11_15_000000_create_locations_table.php b/TestSetup/Database/Migrations/2019_11_15_000000_create_locations_table.php index 972eadf..7e722ea 100644 --- a/TestSetup/Database/Migrations/2019_11_15_000000_create_locations_table.php +++ b/TestSetup/Database/Migrations/2019_11_15_000000_create_locations_table.php @@ -6,7 +6,7 @@ use LaravelFreelancerNL\Aranguent\Facades\Schema; use LaravelFreelancerNL\Aranguent\Schema\Blueprint; -return new class () extends Migration { +return new class extends Migration { /** * Run the migrations. * diff --git a/TestSetup/Database/Migrations/2019_11_15_000000_create_taggables_table.php b/TestSetup/Database/Migrations/2019_11_15_000000_create_taggables_table.php index 75b9fff..ea23729 100644 --- a/TestSetup/Database/Migrations/2019_11_15_000000_create_taggables_table.php +++ b/TestSetup/Database/Migrations/2019_11_15_000000_create_taggables_table.php @@ -6,7 +6,7 @@ use LaravelFreelancerNL\Aranguent\Facades\Schema; use LaravelFreelancerNL\Aranguent\Schema\Blueprint; -return new class () extends Migration { +return new class extends Migration { /** * Run the migrations. * diff --git a/TestSetup/Database/Migrations/2019_11_15_000000_create_tags_table.php b/TestSetup/Database/Migrations/2019_11_15_000000_create_tags_table.php index fd1c6ce..f59fd70 100644 --- a/TestSetup/Database/Migrations/2019_11_15_000000_create_tags_table.php +++ b/TestSetup/Database/Migrations/2019_11_15_000000_create_tags_table.php @@ -6,7 +6,7 @@ use LaravelFreelancerNL\Aranguent\Facades\Schema; use LaravelFreelancerNL\Aranguent\Schema\Blueprint; -return new class () extends Migration { +return new class extends Migration { /** * Run the migrations. * diff --git a/TestSetup/Database/Migrations/2021_11_22_145621_create_house_view.php b/TestSetup/Database/Migrations/2021_11_22_145621_create_house_view.php index dc29fbb..64ca0c6 100644 --- a/TestSetup/Database/Migrations/2021_11_22_145621_create_house_view.php +++ b/TestSetup/Database/Migrations/2021_11_22_145621_create_house_view.php @@ -5,7 +5,7 @@ use Illuminate\Database\Migrations\Migration; use LaravelFreelancerNL\Aranguent\Facades\Schema; -return new class () extends Migration { +return new class extends Migration { /** * Run the migrations. * diff --git a/TestSetup/Database/Migrations/2024_01_04_145621_create_house_search_alias_view.php b/TestSetup/Database/Migrations/2024_01_04_145621_create_house_search_alias_view.php index e306ba6..3fa21e5 100644 --- a/TestSetup/Database/Migrations/2024_01_04_145621_create_house_search_alias_view.php +++ b/TestSetup/Database/Migrations/2024_01_04_145621_create_house_search_alias_view.php @@ -6,7 +6,7 @@ use LaravelFreelancerNL\Aranguent\Facades\Schema; use LaravelFreelancerNL\Aranguent\Schema\Blueprint; -return new class () extends Migration { +return new class extends Migration { /** * Run the migrations. * diff --git a/TestSetup/Database/Migrations/2024_08_05_145621_create_events_table.php b/TestSetup/Database/Migrations/2024_08_05_145621_create_events_table.php index c90a64f..58c1c92 100644 --- a/TestSetup/Database/Migrations/2024_08_05_145621_create_events_table.php +++ b/TestSetup/Database/Migrations/2024_08_05_145621_create_events_table.php @@ -6,7 +6,7 @@ use LaravelFreelancerNL\Aranguent\Facades\Schema; use LaravelFreelancerNL\Aranguent\Schema\Blueprint; -return new class () extends Migration { +return new class extends Migration { /** * Run the migrations. * diff --git a/bin/qa.sh b/bin/qa.sh index 1bb9c4a..0b2e54f 100755 --- a/bin/qa.sh +++ b/bin/qa.sh @@ -9,6 +9,7 @@ echo "Run PHPStan" ./vendor/bin/phpstan analyse -c phpstan.neon echo "Test package from within phpunit" +./vendor/bin/testbench convert:migrations ./vendor/bin/testbench migrate:fresh --path=TestSetup/Database/Migrations --path=vendor/orchestra/testbench-core/laravel/migrations/ --realpath --seed ./vendor/bin/testbench package:test --coverage --min=80 tests diff --git a/src/Concerns/RunsQueries.php b/src/Concerns/RunsQueries.php index 9f80f54..bc4c679 100644 --- a/src/Concerns/RunsQueries.php +++ b/src/Concerns/RunsQueries.php @@ -295,7 +295,7 @@ protected function run($query, $bindings, Closure $callback) $this->logQuery( $query, $bindings, - $this->getElapsedTime((int) $start), + $this->getElapsedTime($start), ); return $result; diff --git a/src/Connection.php b/src/Connection.php index bf2584b..d9c1cde 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -202,7 +202,7 @@ protected function escapeBinary($value) */ public function escape($value, $binary = false) { - return match(gettype($value)) { + return match (gettype($value)) { 'array' => $this->escapeArray($value), 'boolean' => $this->escapeBool($value), 'double' => (string) $value, @@ -249,7 +249,7 @@ protected function escapeString($value, bool $binary = false) */ protected function escapeArray(array $array): string { - foreach($array as $key => $value) { + foreach ($array as $key => $value) { $array[$key] = $this->escape($value); } diff --git a/src/Eloquent/Casts/AsArrayObject.php b/src/Eloquent/Casts/AsArrayObject.php index 79592f1..7cb56fe 100644 --- a/src/Eloquent/Casts/AsArrayObject.php +++ b/src/Eloquent/Casts/AsArrayObject.php @@ -21,7 +21,7 @@ class AsArrayObject extends IlluminateAsArrayObject */ public static function castUsing(array $arguments) { - return new class () implements CastsAttributes { + return new class implements CastsAttributes { public function get($model, $key, $value, $attributes) { if (! isset($attributes[$key])) { diff --git a/src/Eloquent/Concerns/IsAranguentModel.php b/src/Eloquent/Concerns/IsAranguentModel.php index 37a63ee..15755ba 100644 --- a/src/Eloquent/Concerns/IsAranguentModel.php +++ b/src/Eloquent/Concerns/IsAranguentModel.php @@ -34,7 +34,9 @@ protected function insertAndSetId(IlluminateEloquentBuilder $query, $attributes) $matches = []; preg_match('/\/(.*)$/', $id, $matches); - $this->setAttribute('id', $matches[1]); + // We know the exact string format for $matches when the attribute is _id + /** @var array{0: string, 1: string} $matches */ + $this->setAttribute('id', $matches[1]); // @phpstan-ignore arrayUnpacking.stringOffset } if ($keyName === 'id' || $keyName === '_key') { $this->updateIdWithKey($id); diff --git a/src/Providers/MigrationServiceProvider.php b/src/Providers/MigrationServiceProvider.php index 919b412..01f2920 100644 --- a/src/Providers/MigrationServiceProvider.php +++ b/src/Providers/MigrationServiceProvider.php @@ -40,7 +40,7 @@ public function __construct($app) { parent::__construct($app); - foreach($this->aliases as $key => $alias) { + foreach ($this->aliases as $key => $alias) { $this->aliases[$key] = $alias; } } diff --git a/src/Query/Concerns/BuildsGroups.php b/src/Query/Concerns/BuildsGroups.php index 8929433..45922ab 100644 --- a/src/Query/Concerns/BuildsGroups.php +++ b/src/Query/Concerns/BuildsGroups.php @@ -144,7 +144,7 @@ public function havingRaw($sql, array $bindings = [], $boolean = 'and') public function forNestedWhere($aliases = []) { $query = $this->newQuery(); - foreach($aliases as $alias) { + foreach ($aliases as $alias) { $query->groups[] = $alias; } diff --git a/src/Query/Concerns/BuildsSearches.php b/src/Query/Concerns/BuildsSearches.php index 9ed55b5..acaaf08 100644 --- a/src/Query/Concerns/BuildsSearches.php +++ b/src/Query/Concerns/BuildsSearches.php @@ -26,7 +26,7 @@ public function searchView( ): IlluminateQueryBuilder { assert($this->grammar instanceof Grammar); - if(!is_array($fields)) { + if (!is_array($fields)) { $fields = Arr::wrap($fields); } $fields = $this->grammar->convertJsonFields($fields); diff --git a/src/Query/Concerns/BuildsUpdates.php b/src/Query/Concerns/BuildsUpdates.php index 3d904eb..e2688bf 100644 --- a/src/Query/Concerns/BuildsUpdates.php +++ b/src/Query/Concerns/BuildsUpdates.php @@ -22,7 +22,7 @@ trait BuildsUpdates */ protected function prepareValuesForUpdate(array $values) { - foreach($values as $key => $value) { + foreach ($values as $key => $value) { if ($value instanceof Expression) { $values[$key] = $value->getValue($this->grammar); @@ -160,13 +160,13 @@ public function upsert(array $values, $uniqueBy, $update = null) $values = [$values]; } - foreach($values as $key => $value) { + foreach ($values as $key => $value) { $values[$key] = $this->grammar->convertJsonFields($value); $values[$key] = $this->convertIdToKey($values[$key]); $values[$key] = Arr::undot($values[$key]); } - foreach($values as $key => $value) { + foreach ($values as $key => $value) { foreach ($value as $dataKey => $data) { $values[$key][$dataKey] = $this->bindValue($data, 'upsert'); } diff --git a/src/Query/Concerns/BuildsWheres.php b/src/Query/Concerns/BuildsWheres.php index d516543..e4dec60 100644 --- a/src/Query/Concerns/BuildsWheres.php +++ b/src/Query/Concerns/BuildsWheres.php @@ -92,7 +92,7 @@ public function validateOperator(mixed $operator, mixed $value): array if ($this->invalidOperator($operator)) { [$value, $operator] = [$operator, '==']; } - return array($value, $operator); + return [$value, $operator]; } /** diff --git a/src/Query/Concerns/CompilesColumns.php b/src/Query/Concerns/CompilesColumns.php index 8270da8..93e8358 100644 --- a/src/Query/Concerns/CompilesColumns.php +++ b/src/Query/Concerns/CompilesColumns.php @@ -211,7 +211,7 @@ protected function cleanAlias(IlluminateQueryBuilder $query, int|null|string $al $elements = explode('.', $alias); - if( + if ( !$query->isTable($elements[0]) && !$query->isVariable($elements[0]) ) { diff --git a/src/Query/Concerns/CompilesDataManipulations.php b/src/Query/Concerns/CompilesDataManipulations.php index 9d0a7b7..aa726e7 100644 --- a/src/Query/Concerns/CompilesDataManipulations.php +++ b/src/Query/Concerns/CompilesDataManipulations.php @@ -107,7 +107,7 @@ public function compileInsertUsing(IlluminateQueryBuilder $query, array $columns if ($insertDoc === '') { $insertValues = []; - foreach($columns as $column) { + foreach ($columns as $column) { $insertValues[$column] = $this->normalizeColumnReferences($query, $column, 'docs'); } $insertDoc = $this->generateAqlObject($insertValues); @@ -129,7 +129,7 @@ public function compileInsertUsing(IlluminateQueryBuilder $query, array $columns protected function createUpdateObject($values) { $valueStrings = []; - foreach($values as $key => $value) { + foreach ($values as $key => $value) { if (is_array($value)) { $valueStrings[] = $key . ': ' . $this->createUpdateObject($value); @@ -188,19 +188,19 @@ public function compileUpdate(IlluminateQueryBuilder $query, array|string $value public function compileUpsert(IlluminateQueryBuilder $query, array $values, array $uniqueBy, array $update) { $searchFields = []; - foreach($uniqueBy as $field) { + foreach ($uniqueBy as $field) { $searchFields[$field] = 'doc.' . $field; } $searchObject = $this->generateAqlObject($searchFields); $updateFields = []; - foreach($update as $field) { + foreach ($update as $field) { $updateFields[$field] = 'doc.' . $field; } $updateObject = $this->generateAqlObject($updateFields); $valueObjects = []; - foreach($values as $data) { + foreach ($values as $data) { $valueObjects[] = $this->generateAqlObject($data); } diff --git a/src/Query/Concerns/CompilesGroups.php b/src/Query/Concerns/CompilesGroups.php index 4fe54ad..b5ad17c 100644 --- a/src/Query/Concerns/CompilesGroups.php +++ b/src/Query/Concerns/CompilesGroups.php @@ -64,7 +64,7 @@ protected function keepColumns(IlluminateQueryBuilder $query, $groups) return []; } $tempGroups = []; - foreach($groups as $group) { + foreach ($groups as $group) { if ($group instanceof Expression) { $tempGroups[] = $this->extractGroupVariable($group); continue; diff --git a/src/Query/Concerns/CompilesJoins.php b/src/Query/Concerns/CompilesJoins.php index ae133e8..56b4ae7 100644 --- a/src/Query/Concerns/CompilesJoins.php +++ b/src/Query/Concerns/CompilesJoins.php @@ -20,13 +20,15 @@ public function extractTableAndAlias(Builder $query, $join): array { if ($join->table instanceof Expression) { $tableParts = []; - preg_match("/(^.*) as (.*?)$/", (string) $join->table->getValue($query->grammar), $tableParts); - $table = $tableParts[1]; - $alias = $tableParts[2]; - $query->registerTableAlias($join->table, $alias); + if (preg_match("/(^.*) as (.*?)$/", (string) $join->table->getValue($query->grammar), $tableParts)) { + $table = $tableParts[1]; + $alias = $tableParts[2]; - return [$table, $alias]; + $query->registerTableAlias($join->table, $alias); + + return [$table, $alias]; + } } $table = (string) $this->wrapTable($join->table); diff --git a/src/Query/Concerns/ConvertsIdToKey.php b/src/Query/Concerns/ConvertsIdToKey.php index 59ab076..d3a48d6 100644 --- a/src/Query/Concerns/ConvertsIdToKey.php +++ b/src/Query/Concerns/ConvertsIdToKey.php @@ -9,7 +9,7 @@ trait ConvertsIdToKey public function convertIdToKey(mixed $data): mixed { if (is_array($data) && array_is_list($data)) { - foreach($data as $key => $value) { + foreach ($data as $key => $value) { $data[$key] = $this->convertIdInString($value); } return $data; diff --git a/src/Query/Concerns/HandlesAliases.php b/src/Query/Concerns/HandlesAliases.php index 5a0eee8..4c2357c 100644 --- a/src/Query/Concerns/HandlesAliases.php +++ b/src/Query/Concerns/HandlesAliases.php @@ -182,12 +182,13 @@ public function registerTableAlias(string|Expression $table, string $alias = nul } /** @phpstan-ignore-next-line */ - if ($alias == null && stripos($table, ' as ') !== false) { + if ($alias == null && is_string($table) && stripos($table, ' as ') !== false) { $tableParts = []; - /** @phpstan-ignore-next-line */ - preg_match("/(^.*) as (.*?)$/", $table, $tableParts); - $table = $tableParts[1]; - $alias = $tableParts[2]; + + if (preg_match("/(^.*) as (.*?)$/", $table, $tableParts)) { + $table = $tableParts[1]; + $alias = $tableParts[2]; + } } if ($alias == null) { diff --git a/src/Query/Concerns/HandlesAqlGrammar.php b/src/Query/Concerns/HandlesAqlGrammar.php index 6028e0a..cfc0794 100644 --- a/src/Query/Concerns/HandlesAqlGrammar.php +++ b/src/Query/Concerns/HandlesAqlGrammar.php @@ -125,7 +125,7 @@ public function wrap($value) } if (is_array($value)) { - foreach($value as $key => $subvalue) { + foreach ($value as $key => $subvalue) { $value[$key] = $this->wrap($subvalue); } return $value; @@ -220,7 +220,7 @@ public function generateAqlObject(array $data): string */ protected function generateAqlObjectString(array $data): string { - foreach($data as $key => $value) { + foreach ($data as $key => $value) { $prefix = $this->wrapAttribute($key) . ': '; if (is_numeric($key)) { @@ -269,7 +269,7 @@ public function substituteBindingsIntoRawSql($aql, $bindings) $bindings = array_reverse($bindings); - foreach($bindings as $key => $value) { + foreach ($bindings as $key => $value) { $pattern = '/(@' . $key . ')(?![^a-zA-Z_ ,\}\]])/'; $aql = (string) preg_replace( $pattern, @@ -289,7 +289,7 @@ public function substituteBindingsIntoRawSql($aql, $bindings) */ public function isJsonSelector($value) { - if(!is_string($value)) { + if (!is_string($value)) { return false; } @@ -319,7 +319,7 @@ public function convertJsonFields(mixed $data): mixed */ public function convertJsonValuesToDotNotation(array $fields): array { - foreach($fields as $key => $value) { + foreach ($fields as $key => $value) { if ($this->isJsonSelector($value)) { $fields[$key] = str_replace('->', '.', $value); } @@ -333,7 +333,7 @@ public function convertJsonValuesToDotNotation(array $fields): array */ public function convertJsonKeysToDotNotation(array $fields): array { - foreach($fields as $key => $value) { + foreach ($fields as $key => $value) { if ($this->isJsonSelector($key)) { $fields[str_replace('->', '.', $key)] = $value; unset($fields[$key]); diff --git a/src/Query/Grammar.php b/src/Query/Grammar.php index 12a106b..189ba82 100644 --- a/src/Query/Grammar.php +++ b/src/Query/Grammar.php @@ -419,7 +419,7 @@ public function compileSearch(IlluminateQueryBuilder $query, array $search) } $predicates = []; - foreach($search['fields'] as $field) { + foreach ($search['fields'] as $field) { $predicates[] = $this->normalizeColumn($query, $field) . ' IN TOKENS(' . $search['searchText'] . ', \'' . $search['analyzer'] . '\')'; } diff --git a/src/Testing/DatabaseMigrations.php b/src/Testing/DatabaseMigrations.php index ea3363b..aaedcac 100644 --- a/src/Testing/DatabaseMigrations.php +++ b/src/Testing/DatabaseMigrations.php @@ -40,7 +40,7 @@ protected function setMigrationPaths() { $migrationSettings = []; - if(property_exists($this, 'realPath')) { + if (property_exists($this, 'realPath')) { $migrationSettings['--realpath'] = $this->realPath ?? false; } diff --git a/src/Testing/DatabaseTruncation.php b/src/Testing/DatabaseTruncation.php index 04bb172..7edf921 100644 --- a/src/Testing/DatabaseTruncation.php +++ b/src/Testing/DatabaseTruncation.php @@ -40,7 +40,7 @@ protected function setMigrationPaths() { $migrationSettings = []; - if(property_exists($this, 'realPath')) { + if (property_exists($this, 'realPath')) { $migrationSettings['--realpath'] = $this->realPath ?? false; } diff --git a/src/Testing/RefreshDatabase.php b/src/Testing/RefreshDatabase.php index 1cfece4..9762dd7 100644 --- a/src/Testing/RefreshDatabase.php +++ b/src/Testing/RefreshDatabase.php @@ -79,7 +79,7 @@ protected function setMigrationPaths() { $migrationSettings = []; - if(property_exists($this, 'realPath')) { + if (property_exists($this, 'realPath')) { $migrationSettings['--realpath'] = $this->realPath ?? false; } diff --git a/src/helpers.php b/src/helpers.php index df6aad5..0b0085e 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -49,7 +49,7 @@ function isDotString(string $string): bool if (!function_exists('mapObjectToArray')) { function mapObjectToArray(mixed $value): mixed { - if(!is_object($value) && !is_array($value)) { + if (!is_object($value) && !is_array($value)) { return $value; } diff --git a/tests/Console/ModelMakeCommandTest.php b/tests/Console/ModelMakeCommandTest.php index 11b6e12..f9ac39d 100644 --- a/tests/Console/ModelMakeCommandTest.php +++ b/tests/Console/ModelMakeCommandTest.php @@ -71,7 +71,7 @@ $migrationFiles = scandir($migrationPath); - foreach($migrationFiles as $file) { + foreach ($migrationFiles as $file) { if (in_array($file, ['.', '..', '.gitkeep'])) { continue; } @@ -90,7 +90,7 @@ $migrationFiles = scandir($migrationPath); - foreach($migrationFiles as $file) { + foreach ($migrationFiles as $file) { if (in_array($file, ['.', '..', '.gitkeep'])) { continue; } diff --git a/tests/Migrations/MigrationRepositoryTest.php b/tests/Migrations/MigrationRepositoryTest.php index e548f95..d2f0fa0 100644 --- a/tests/Migrations/MigrationRepositoryTest.php +++ b/tests/Migrations/MigrationRepositoryTest.php @@ -11,7 +11,7 @@ afterEach(function () { $migrations = $this->databaseMigrationRepository->getMigrations(12); - foreach($migrations as $migration) { + foreach ($migrations as $migration) { $this->databaseMigrationRepository->delete($migration); } }); @@ -92,7 +92,7 @@ "getMigrationBatches2" => 33, ]; - foreach($batches as $migration => $batch) { + foreach ($batches as $migration => $batch) { $this->databaseMigrationRepository->log($migration, $batch); } diff --git a/tests/Query/DataRetrievalTest.php b/tests/Query/DataRetrievalTest.php index 845c310..c3df3c8 100644 --- a/tests/Query/DataRetrievalTest.php +++ b/tests/Query/DataRetrievalTest.php @@ -101,7 +101,7 @@ function ($query) use ($residenceId) { foreach ($characters as $character) { $count++; } - if($count > 20) { + if ($count > 20) { return false; } return true; diff --git a/tests/Query/DebugTest.php b/tests/Query/DebugTest.php index b555077..209c827 100644 --- a/tests/Query/DebugTest.php +++ b/tests/Query/DebugTest.php @@ -68,14 +68,14 @@ $query = DB::table('characters'); $query->where('name', 'Gilly'); - for($i = 0; $i < 9; $i++) { + for ($i = 0; $i < 9; $i++) { $query->orWhere('name', $names[$i]); } $aql = $query->toRawSql(); $rawAql = 'FOR characterDoc IN characters FILTER `characterDoc`.`name` == "Gilly"'; - for($i = 0; $i < 9; $i++) { + for ($i = 0; $i < 9; $i++) { $rawAql .= ' or `characterDoc`.`name` == "' . $names[$i] . '"'; } $rawAql .= ' RETURN characterDoc'; diff --git a/tests/QueryExceptionTest.php b/tests/QueryExceptionTest.php index a185917..c20489c 100644 --- a/tests/QueryExceptionTest.php +++ b/tests/QueryExceptionTest.php @@ -17,14 +17,8 @@ }); test('query exception has correct message', function () { - $this->expectExceptionMessage( - "400 - AQL: syntax error, unexpected identifier near 'this is not AQL' at position 1:1 (while parsing)" - . " (Connection: arangodb,AQL: this is not AQL - Bindings: array (\n" - . " 'testBind' => 'test',\n))", - ); - DB::execute('this is not AQL', ['testBind' => 'test']); -}); +})->throws(QueryException::class, 'this is not AQL'); test('query exception without binds', function () { expect(fn() => DB::execute("this is not AQL", []))->toThrow(QueryException::class); diff --git a/tests/Schema/SchemaBuilderTest.php b/tests/Schema/SchemaBuilderTest.php index a94ecca..dbf5f55 100644 --- a/tests/Schema/SchemaBuilderTest.php +++ b/tests/Schema/SchemaBuilderTest.php @@ -28,7 +28,7 @@ try { Schema::hasTable('dummy'); - } catch(QueryException $e) { + } catch (QueryException $e) { expect($e)->toBeInstanceOf(QueryException::class); } config()->set('database.connections.arangodb.database', $oldDatabase); diff --git a/tests/Schema/TableTest.php b/tests/Schema/TableTest.php index f4eaa0a..516d297 100644 --- a/tests/Schema/TableTest.php +++ b/tests/Schema/TableTest.php @@ -1,7 +1,7 @@ getSchemaBuilder(); diff --git a/tests/TestCase.php b/tests/TestCase.php index e712c43..208e1e3 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -208,7 +208,7 @@ protected function defineEnvironment($app) public function clearDatabase() { $collections = $this->schemaManager->getCollections(true); - foreach($collections as $collection) { + foreach ($collections as $collection) { $this->schemaManager->deleteCollection($collection->name); } } From f5e06250767bf9863c315ee02afdf416b093cf68 Mon Sep 17 00:00:00 2001 From: Laravel Freelancer NL <36150929+LaravelFreelancerNL@users.noreply.github.com> Date: Sun, 3 Nov 2024 22:59:24 +0100 Subject: [PATCH 04/34] Add castAsJson connection parameter (#164) --- src/Testing/Concerns/InteractsWithDatabase.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Testing/Concerns/InteractsWithDatabase.php b/src/Testing/Concerns/InteractsWithDatabase.php index 2da9780..e1073cd 100644 --- a/src/Testing/Concerns/InteractsWithDatabase.php +++ b/src/Testing/Concerns/InteractsWithDatabase.php @@ -14,9 +14,10 @@ trait InteractsWithDatabase * No cast is necessary as json is a first class citizen in ArangoDB. * * @param array|object|string $value + * @param string|null $connection * @return \Illuminate\Contracts\Database\Query\Expression */ - public function castAsJson($value) + public function castAsJson($value, $connection = null) { return DB::raw($value); } From 5f36debc904979b7506d4af7619b7f45e019667b Mon Sep 17 00:00:00 2001 From: Laravel Freelancer NL <36150929+LaravelFreelancerNL@users.noreply.github.com> Date: Sun, 10 Nov 2024 17:11:58 +0100 Subject: [PATCH 05/34] Confirmed proper functioning of whereNot/orWhereNot (#165) --- docs/compatibility-list.md | 2 +- phpunit.xml | 1 + tests/Query/WheresTest.php | 88 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 1 deletion(-) diff --git a/docs/compatibility-list.md b/docs/compatibility-list.md index 4a12ccd..3a23156 100644 --- a/docs/compatibility-list.md +++ b/docs/compatibility-list.md @@ -74,7 +74,7 @@ rightJoin / rightJoinSub / joinWhere? where / orWhere / whereNot / orWhereNot / whereColumn / whereExists whereBetween / whereNotBetween / whereBetweenColumns / whereNotBetweenColumns / whereJsonContains / whereJsonLength / -whereIn / whereNotIn / whereNull / whereNotNull / +whereIn / whereNotIn / whereNot / orWhereNot / whereNull / whereNotNull / whereDate / whereMonth / whereDay / whereYear / whereTime / whereRaw (use AQL) / whereAll / orWhereAll / whereAny / orWhereAny diff --git a/phpunit.xml b/phpunit.xml index 72599c4..5bc8a49 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -30,6 +30,7 @@ + diff --git a/tests/Query/WheresTest.php b/tests/Query/WheresTest.php index bd5da57..317311f 100644 --- a/tests/Query/WheresTest.php +++ b/tests/Query/WheresTest.php @@ -530,3 +530,91 @@ expect($results->count())->toBe(2); expect(($results->first())->name)->toBe('Stark'); }); + +test('basic whereNot', function () { + $builder = getBuilder(); + $builder->select('*')->from('characters')->where('surname', 'Lannister')->whereNot('alive', true); + + $this->assertSame( + 'FOR characterDoc IN characters FILTER `characterDoc`.`surname` == @' + . $builder->getQueryId() + . '_where_1 and not `characterDoc`.`alive` == @' + . $builder->getQueryId() + . '_where_2 RETURN characterDoc', + $builder->toSql(), + ); +}); + +test('whereNot nested', function () { + $query = getBuilder(); + $query = $query + ->select('*') + ->from('characters') + ->where('alive', true) + ->whereNot(function ($query) { + $query->where('surname', 'lannister') + ->orWhere('age', '<', 20); + }); + + + $binds = $query->getBindings(); + $bindKeys = array_keys($binds); + + $this->assertSame( + 'FOR characterDoc IN characters FILTER `characterDoc`.`alive` == @' . $bindKeys[0] + . ' and not ( `characterDoc`.`surname` == @' . $bindKeys[1] + . ' or `characterDoc`.`age` < @' . $bindKeys[2] + . ') RETURN characterDoc', + $query->toSql(), + ); +}); + +test('whereNot query results', function () { + $results = \DB::table('characters') + ->where('alive', true) + ->whereNot(function ($query) { + $query->where('surname', 'Lannister') + ->orWhere('age', '<', 20); + })->get(); + + expect($results->count())->toBe(3); +}); + +test('basic orWhereNot', function () { + $builder = getBuilder(); + $builder->select('*')->from('characters')->where('alive', true)->orWhereNot('surname', 'Lannister'); + + $this->assertSame( + 'FOR characterDoc IN characters FILTER `characterDoc`.`alive` == @' + . $builder->getQueryId() + . '_where_1 or not `characterDoc`.`surname` == @' + . $builder->getQueryId() + . '_where_2 RETURN characterDoc', + $builder->toSql(), + ); +}); + + +test('orWhereNot query results', function () { + $results = \DB::table('characters') + ->where('alive', true) + ->orWhereNot('surname', 'Lannister') + ->get(); + + ray($results); + + expect($results->count())->toBe(27); +}); + +test('nest whereNot & orWhereNot', function () { + $builder = \DB::table('characters') + ->where('alive', true) + ->where(function ($query) { + $query->whereNot('surname', 'Lannister') + ->orWhereNot('age', '<', 20); + }); + + $results = $builder->get(); + + expect($results->count())->toBe(27); +}); From 9e1b965c52d865c0b655b8cff761d0a384f66417 Mon Sep 17 00:00:00 2001 From: Laravel Freelancer NL <36150929+LaravelFreelancerNL@users.noreply.github.com> Date: Sun, 10 Nov 2024 19:24:22 +0100 Subject: [PATCH 06/34] Confirmed proper functioning of whereNone/orWhereNone (#168) --- docs/compatibility-list.md | 3 ++- tests/Query/WheresTest.php | 43 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/docs/compatibility-list.md b/docs/compatibility-list.md index 3a23156..1ea57d7 100644 --- a/docs/compatibility-list.md +++ b/docs/compatibility-list.md @@ -74,7 +74,8 @@ rightJoin / rightJoinSub / joinWhere? where / orWhere / whereNot / orWhereNot / whereColumn / whereExists whereBetween / whereNotBetween / whereBetweenColumns / whereNotBetweenColumns / whereJsonContains / whereJsonLength / -whereIn / whereNotIn / whereNot / orWhereNot / whereNull / whereNotNull / +whereIn / whereNotIn / whereNone / orWhereNone / whereNot / orWhereNot / +whereNull / whereNotNull / whereDate / whereMonth / whereDay / whereYear / whereTime / whereRaw (use AQL) / whereAll / orWhereAll / whereAny / orWhereAny diff --git a/tests/Query/WheresTest.php b/tests/Query/WheresTest.php index 317311f..f3e244e 100644 --- a/tests/Query/WheresTest.php +++ b/tests/Query/WheresTest.php @@ -531,6 +531,49 @@ expect(($results->first())->name)->toBe('Stark'); }); +test('whereNone', function () { + $query = \DB::table('houses') + ->whereNone(['en.coat-of-arms', 'en.description'], 'LIKE', '%war%'); + + $binds = $query->getBindings(); + $bindKeys = array_keys($binds); + + $this->assertSame( + 'FOR houseDoc IN houses FILTER not ( `houseDoc`.`en`.`coat-of-arms` LIKE @' . $bindKeys[0] + . ' or `houseDoc`.`en`.`description` LIKE @' . $bindKeys[1] + . ') RETURN houseDoc', + $query->toSql(), + ); + + $results = $query->get(); + expect($results->count())->toBe(1); + expect(($results->first())->name)->toBe('Targaryen'); +}); + +test('orWhereNone', function () { + $query = \DB::table('houses') + ->where('name', 'Stark') + ->orWhereNone(['en.coat-of-arms', 'en.description'], 'LIKE', '%war%'); + + $binds = $query->getBindings(); + $bindKeys = array_keys($binds); + + $this->assertSame( + 'FOR houseDoc IN houses FILTER `houseDoc`.`name` == @' . $bindKeys[0] + + . ' or not ( `houseDoc`.`en`.`coat-of-arms` LIKE @' . $bindKeys[1] + . ' or `houseDoc`.`en`.`description` LIKE @' . $bindKeys[2] + . ') RETURN houseDoc', + $query->toSql(), + ); + + $results = $query->get(); + expect($results->count())->toBe(2); + expect(($results->first())->name)->toBe('Stark'); +}); + + + test('basic whereNot', function () { $builder = getBuilder(); $builder->select('*')->from('characters')->where('surname', 'Lannister')->whereNot('alive', true); From 8b405eb2a40f8458a816c02d1def56f5304403b7 Mon Sep 17 00:00:00 2001 From: Laravel Freelancer NL <36150929+LaravelFreelancerNL@users.noreply.github.com> Date: Sun, 10 Nov 2024 20:49:47 +0100 Subject: [PATCH 07/34] 167 support wherelike orwherelike wherenotlike orwherenotlike (#169) * feature: Added support for whereLike/orWhereLike/whereNotLike/orWhereNotLike * docs: added whereLike / orWhereLike / whereNotLike / orWhereNotLike --- docs/compatibility-list.md | 4 +- src/Query/Concerns/BuildsWheres.php | 21 +++++++ src/Query/Concerns/CompilesFilters.php | 30 ++++++++++ tests/Query/WheresTest.php | 78 +++++++++++++++++++++++++- 4 files changed, 130 insertions(+), 3 deletions(-) diff --git a/docs/compatibility-list.md b/docs/compatibility-list.md index 1ea57d7..aa39b30 100644 --- a/docs/compatibility-list.md +++ b/docs/compatibility-list.md @@ -74,7 +74,9 @@ rightJoin / rightJoinSub / joinWhere? where / orWhere / whereNot / orWhereNot / whereColumn / whereExists whereBetween / whereNotBetween / whereBetweenColumns / whereNotBetweenColumns / whereJsonContains / whereJsonLength / -whereIn / whereNotIn / whereNone / orWhereNone / whereNot / orWhereNot / +whereIn / whereNotIn / +whereLike / orWhereLike / whereNotLike / orWhereNotLike / +whereNone / orWhereNone / whereNot / orWhereNot / whereNull / whereNotNull / whereDate / whereMonth / whereDay / whereYear / whereTime / whereRaw (use AQL) / diff --git a/src/Query/Concerns/BuildsWheres.php b/src/Query/Concerns/BuildsWheres.php index e4dec60..55d302e 100644 --- a/src/Query/Concerns/BuildsWheres.php +++ b/src/Query/Concerns/BuildsWheres.php @@ -435,6 +435,27 @@ public function whereJsonLength($column, $operator, $value = null, $boolean = 'a return $this; } + /** + * Add a "where like" clause to the query. + * + * @param \Illuminate\Contracts\Database\Query\Expression|string $column + * @param string $value + * @param bool $caseSensitive + * @param string $boolean + * @param bool $not + * @return $this + */ + public function whereLike($column, $value, $caseSensitive = false, $boolean = 'and', $not = false) + { + $type = 'Like'; + + $value = $this->bindValue($value); + + $this->wheres[] = compact('type', 'column', 'value', 'caseSensitive', 'boolean', 'not'); + + return $this; + } + /** * Add a "where null" clause to the query. * diff --git a/src/Query/Concerns/CompilesFilters.php b/src/Query/Concerns/CompilesFilters.php index d38b770..291ed47 100644 --- a/src/Query/Concerns/CompilesFilters.php +++ b/src/Query/Concerns/CompilesFilters.php @@ -405,6 +405,36 @@ protected function filterYear(IlluminateQueryBuilder $query, $filter) return implode(' ', $predicate); } + /** + * Compile a filter month clause. + * + * @param IlluminateQueryBuilder $query + * @param array $filter + * @return string + * @throws \Exception + */ + protected function filterLike(IlluminateQueryBuilder $query, $filter) + { + $column = $this->normalizeColumn($query, $filter['column']); + $value = $this->parameter($filter['value']); + + $predicate = []; + + $filter = $this->normalizeOperator($filter); + + $predicate[0] = ($filter['caseSensitive']) ? $column : 'LOWER(' . $column . ')'; + $predicate[1] = 'LIKE'; + $predicate[2] = ($filter['caseSensitive']) ? $value : 'LOWER(' . $value . ')'; + + $result = implode(' ', $predicate); + + if ($filter['not']) { + $result = 'NOT (' . $result . ')'; + } + + return $result; + } + /** * Compile a filter month clause. * diff --git a/tests/Query/WheresTest.php b/tests/Query/WheresTest.php index f3e244e..d243b7b 100644 --- a/tests/Query/WheresTest.php +++ b/tests/Query/WheresTest.php @@ -644,8 +644,6 @@ ->orWhereNot('surname', 'Lannister') ->get(); - ray($results); - expect($results->count())->toBe(27); }); @@ -661,3 +659,79 @@ expect($results->count())->toBe(27); }); + +test('whereLike', function () { + $query = \DB::table('houses') + ->whereLike('en.coat-of-arms', '%dragon%'); + + $binds = $query->getBindings(); + $bindKeys = array_keys($binds); + + $this->assertSame( + 'FOR houseDoc IN houses FILTER LOWER(`houseDoc`.`en`.`coat-of-arms`) LIKE LOWER(@' . $bindKeys[0] + . ') RETURN houseDoc', + $query->toSql(), + ); + + $results = $query->get(); + expect($results->count())->toBe(1); + expect(($results->first())->name)->toBe('Targaryen'); +}); + +test('orWhereLike', function () { + $query = \DB::table('houses') + ->where('name', 'Stark') + ->orWhereLike('en.coat-of-arms', '%dragon%'); + + $binds = $query->getBindings(); + $bindKeys = array_keys($binds); + + $this->assertSame( + 'FOR houseDoc IN houses FILTER `houseDoc`.`name` == @' . $bindKeys[0] + . ' or LOWER(`houseDoc`.`en`.`coat-of-arms`) LIKE LOWER(@' . $bindKeys[1] + . ') RETURN houseDoc', + $query->toSql(), + ); + + $results = $query->get(); + expect($results->count())->toBe(2); + expect(($results->first())->name)->toBe('Stark'); +}); + +test('whereNotLike', function () { + $query = \DB::table('houses') + ->whereNotLike('en.coat-of-arms', '%dragon%'); + + $binds = $query->getBindings(); + $bindKeys = array_keys($binds); + + $this->assertSame( + 'FOR houseDoc IN houses FILTER NOT (LOWER(`houseDoc`.`en`.`coat-of-arms`) LIKE LOWER(@' . $bindKeys[0] + . ')) RETURN houseDoc', + $query->toSql(), + ); + + $results = $query->get(); + expect($results->count())->toBe(2); + expect(($results->first())->name)->toBe('Lannister'); +}); + +test('orWhereNotLike', function () { + $query = \DB::table('houses') + ->where('name', 'Stark') + ->orWhereNotLike('en.coat-of-arms', '%dragon%'); + + $binds = $query->getBindings(); + $bindKeys = array_keys($binds); + + $this->assertSame( + 'FOR houseDoc IN houses FILTER `houseDoc`.`name` == @' . $bindKeys[0] + . ' or NOT (LOWER(`houseDoc`.`en`.`coat-of-arms`) LIKE LOWER(@' . $bindKeys[1] + . ')) RETURN houseDoc', + $query->toSql(), + ); + + $results = $query->get(); + expect($results->count())->toBe(2); + expect(($results->first())->name)->toBe('Lannister'); +}); From e248271fd33191c7c77344f2b7841b47c61b379f Mon Sep 17 00:00:00 2001 From: Laravel Freelancer NL <36150929+LaravelFreelancerNL@users.noreply.github.com> Date: Sat, 23 Nov 2024 17:53:17 +0100 Subject: [PATCH 08/34] Chore/support php84 (#170) * Fixed implicit nullable php 8.4 deprecations * Fixed parameter type mismatch * Removed left over ray call * Suppressed BooleanArgumentFlag phpmd warning * Suppressed CyclomaticComplexity phpmd warning * Suppressed UnusedFormalParameter phpmd warning * Removed ray call * Added php 8.4 to the test matrix * Added support for testing transactions on multiple connections --- .github/workflows/run-tests.yml | 2 +- src/Eloquent/Builder.php | 1 - src/Eloquent/Concerns/QueriesAranguentRelationships.php | 2 +- src/Eloquent/Relations/Concerns/IsAranguentRelation.php | 2 -- src/Query/Builder.php | 6 +++--- src/Query/Concerns/BuildsWheres.php | 2 ++ src/Query/Concerns/CompilesColumns.php | 6 +++--- src/Query/Concerns/CompilesDataManipulations.php | 6 +++--- src/Query/Concerns/HandlesAliases.php | 9 ++++++--- src/Query/Concerns/HandlesBindings.php | 2 +- src/Testing/Concerns/InteractsWithDatabase.php | 2 ++ src/Testing/DatabaseTransactions.php | 4 +++- src/Testing/RefreshDatabase.php | 4 +++- 13 files changed, 28 insertions(+), 20 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 43e8e33..1840ea4 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: arangodb: ['3.11', '3.12'] - php: ['8.2', '8.3'] + php: ['8.2', '8.3', '8.4'] laravel: ['^11.0'] include: - laravel: '^11.0' diff --git a/src/Eloquent/Builder.php b/src/Eloquent/Builder.php index dd66dec..ad841bb 100644 --- a/src/Eloquent/Builder.php +++ b/src/Eloquent/Builder.php @@ -33,7 +33,6 @@ public function createOrFirst(array $attributes = [], array $values = []) try { return $this->withSavepointIfNeeded(fn() => $this->create(array_merge($attributes, $values))); } catch (UniqueConstraintViolationException $e) { - ray($e); return $this->useWritePdo()->where($attributes)->first() ?? throw $e; } } diff --git a/src/Eloquent/Concerns/QueriesAranguentRelationships.php b/src/Eloquent/Concerns/QueriesAranguentRelationships.php index 7f50367..fa16bc8 100644 --- a/src/Eloquent/Concerns/QueriesAranguentRelationships.php +++ b/src/Eloquent/Concerns/QueriesAranguentRelationships.php @@ -169,7 +169,7 @@ public function withAggregate($relations, $column, $function = null) $query = $relation->getRelationExistenceQuery( $relation->getRelated()->newQuery(), $this, - new Expression($expression), + [$expression], )->setBindings([], 'select'); $query->callScope($constraints); diff --git a/src/Eloquent/Relations/Concerns/IsAranguentRelation.php b/src/Eloquent/Relations/Concerns/IsAranguentRelation.php index 228b322..12448eb 100644 --- a/src/Eloquent/Relations/Concerns/IsAranguentRelation.php +++ b/src/Eloquent/Relations/Concerns/IsAranguentRelation.php @@ -5,7 +5,6 @@ namespace LaravelFreelancerNL\Aranguent\Eloquent\Relations\Concerns; use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Query\Expression; trait IsAranguentRelation { @@ -41,7 +40,6 @@ public function getRelationExistenceCountQuery(Builder $query, Builder $parentQu return $this->getRelationExistenceQuery( $query, $parentQuery, - new Expression('*'), ); } } diff --git a/src/Query/Builder.php b/src/Query/Builder.php index 58cdf02..0d42f88 100644 --- a/src/Query/Builder.php +++ b/src/Query/Builder.php @@ -116,9 +116,9 @@ class Builder extends IlluminateQueryBuilder */ public function __construct( IlluminateConnectionInterface $connection, - IlluminateQueryGrammar $grammar = null, - IlluminateProcessor $processor = null, - AQB $aqb = null, + ?IlluminateQueryGrammar $grammar = null, + ?IlluminateProcessor $processor = null, + ?AQB $aqb = null, ) { assert($connection instanceof IlluminateConnectionInterface); assert($processor instanceof IlluminateProcessor); diff --git a/src/Query/Concerns/BuildsWheres.php b/src/Query/Concerns/BuildsWheres.php index 55d302e..e4ddf55 100644 --- a/src/Query/Concerns/BuildsWheres.php +++ b/src/Query/Concerns/BuildsWheres.php @@ -444,6 +444,8 @@ public function whereJsonLength($column, $operator, $value = null, $boolean = 'a * @param string $boolean * @param bool $not * @return $this + * + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) */ public function whereLike($column, $value, $caseSensitive = false, $boolean = 'and', $not = false) { diff --git a/src/Query/Concerns/CompilesColumns.php b/src/Query/Concerns/CompilesColumns.php index 93e8358..40cd8cd 100644 --- a/src/Query/Concerns/CompilesColumns.php +++ b/src/Query/Concerns/CompilesColumns.php @@ -91,7 +91,7 @@ protected function prepareColumns(IlluminateQueryBuilder $query, array $columns) /** * @throws Exception */ - protected function normalizeColumn(IlluminateQueryBuilder $query, mixed $column, string $table = null): mixed + protected function normalizeColumn(IlluminateQueryBuilder $query, mixed $column, ?string $table = null): mixed { assert($query instanceof Builder); @@ -137,7 +137,7 @@ protected function normalizeColumn(IlluminateQueryBuilder $query, mixed $column, * @return array * @throws Exception */ - protected function normalizeStringColumn(Builder $query, int|string $key, string $column, string $table = null): array + protected function normalizeStringColumn(Builder $query, int|string $key, string $column, ?string $table = null): array { [$column, $alias] = $query->extractAlias($column, $key); @@ -158,7 +158,7 @@ protected function normalizeStringColumn(Builder $query, int|string $key, string * @param string|null $table * @return string */ - protected function normalizeColumnReferences(IlluminateQueryBuilder $query, string $column, string $table = null): string + protected function normalizeColumnReferences(IlluminateQueryBuilder $query, string $column, ?string $table = null): string { assert($query instanceof Builder); diff --git a/src/Query/Concerns/CompilesDataManipulations.php b/src/Query/Concerns/CompilesDataManipulations.php index aa726e7..4c142b0 100644 --- a/src/Query/Concerns/CompilesDataManipulations.php +++ b/src/Query/Concerns/CompilesDataManipulations.php @@ -16,7 +16,7 @@ trait CompilesDataManipulations * @param string|null $bindVar * @return string */ - public function compileInsert(Builder|IlluminateQueryBuilder $query, array $values, string $bindVar = null) + public function compileInsert(Builder|IlluminateQueryBuilder $query, array $values, ?string $bindVar = null) { $table = $this->prefixTable($query->from); @@ -41,7 +41,7 @@ public function compileInsert(Builder|IlluminateQueryBuilder $query, array $valu * @param string|null $bindVar * @return string */ - public function compileInsertGetId(IlluminateQueryBuilder $query, $values, $sequence = '_key', string $bindVar = null) + public function compileInsertGetId(IlluminateQueryBuilder $query, $values, $sequence = '_key', ?string $bindVar = null) { $table = $this->prefixTable($query->from); @@ -68,7 +68,7 @@ public function compileInsertGetId(IlluminateQueryBuilder $query, $values, $sequ * @param array $values * @return string */ - public function compileInsertOrIgnore(IlluminateQueryBuilder $query, array $values, string $bindVar = null) + public function compileInsertOrIgnore(IlluminateQueryBuilder $query, array $values, ?string $bindVar = null) { $table = $this->prefixTable($query->from); diff --git a/src/Query/Concerns/HandlesAliases.php b/src/Query/Concerns/HandlesAliases.php index 4c2357c..6dcd710 100644 --- a/src/Query/Concerns/HandlesAliases.php +++ b/src/Query/Concerns/HandlesAliases.php @@ -45,7 +45,7 @@ public function convertColumnId(array|string|Expression $column): array|string|E * * @throws Exception */ - public function extractAlias(string $entity, int|string $key = null): array + public function extractAlias(string $entity, int|null|string $key = null): array { $results = preg_split("/\sas\s/i", $entity); @@ -155,7 +155,7 @@ public function prefixAlias(string $target, string $value): string /** * @throws Exception */ - public function registerColumnAlias(string $column, string $alias = null): bool + public function registerColumnAlias(string $column, ?string $alias = null): bool { if (preg_match("/\sas\s/i", $column)) { [$column, $alias] = $this->extractAlias($column); @@ -170,7 +170,10 @@ public function registerColumnAlias(string $column, string $alias = null): bool return false; } - public function registerTableAlias(string|Expression $table, string $alias = null): string + /** + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + public function registerTableAlias(string|Expression $table, ?string $alias = null): string { if ($table instanceof Expression && $alias !== null) { $table = 'Expression' . spl_object_id($table); diff --git a/src/Query/Concerns/HandlesBindings.php b/src/Query/Concerns/HandlesBindings.php index 16b28e6..f267b3a 100644 --- a/src/Query/Concerns/HandlesBindings.php +++ b/src/Query/Concerns/HandlesBindings.php @@ -78,7 +78,7 @@ protected function getLastBindVariable(string $type = 'where'): string * @param string|null $type * @return void */ - public function importBindings($query, string $type = null): void + public function importBindings($query, ?string $type = null): void { if ($type) { $this->bindings[$type] = array_merge($this->bindings[$type], $query->bindings[$type]); diff --git a/src/Testing/Concerns/InteractsWithDatabase.php b/src/Testing/Concerns/InteractsWithDatabase.php index e1073cd..c5e36af 100644 --- a/src/Testing/Concerns/InteractsWithDatabase.php +++ b/src/Testing/Concerns/InteractsWithDatabase.php @@ -16,6 +16,8 @@ trait InteractsWithDatabase * @param array|object|string $value * @param string|null $connection * @return \Illuminate\Contracts\Database\Query\Expression + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function castAsJson($value, $connection = null) { diff --git a/src/Testing/DatabaseTransactions.php b/src/Testing/DatabaseTransactions.php index 13933d6..c959bf1 100644 --- a/src/Testing/DatabaseTransactions.php +++ b/src/Testing/DatabaseTransactions.php @@ -22,7 +22,9 @@ public function beginDatabaseTransaction() { $database = $this->app->make('db'); - $this->app->instance('db.transactions', $transactionsManager = new DatabaseTransactionsManager()); + $connections = $this->connectionsToTransact(); + + $this->app->instance('db.transactions', $transactionsManager = new DatabaseTransactionsManager($connections)); foreach ($this->connectionsToTransact() as $name) { $connection = $database->connection($name); diff --git a/src/Testing/RefreshDatabase.php b/src/Testing/RefreshDatabase.php index 9762dd7..282a5dc 100644 --- a/src/Testing/RefreshDatabase.php +++ b/src/Testing/RefreshDatabase.php @@ -22,7 +22,9 @@ public function beginDatabaseTransaction() { $database = $this->app->make('db'); - $this->app->instance('db.transactions', $transactionsManager = new DatabaseTransactionsManager()); + $connections = $this->connectionsToTransact(); + + $this->app->instance('db.transactions', $transactionsManager = new DatabaseTransactionsManager($connections)); foreach ($this->connectionsToTransact() as $name) { $connection = $database->connection($name); From 2342c559b83e7ecc6679fd0077d6b6915fafe5da Mon Sep 17 00:00:00 2001 From: Laravel Freelancer NL <36150929+LaravelFreelancerNL@users.noreply.github.com> Date: Sun, 24 Nov 2024 12:40:24 +0100 Subject: [PATCH 09/34] Added threadCount to connection (#172) --- src/Connection.php | 14 ++++++++++++++ tests/Database/ConnectionTest.php | 8 ++++++++ 2 files changed, 22 insertions(+) diff --git a/src/Connection.php b/src/Connection.php index d9c1cde..cce4336 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -282,4 +282,18 @@ protected function getElapsedTime($start) { return round((microtime(true) - $start) * 1000, 2); } + + /** + * Get the number of open connections for the database. + * + * @return int|null + */ + public function threadCount() + { + if (!$this->arangoClient) { + return null; + } + + return $this->arangoClient->monitor()->getCurrentConnections(); + } } diff --git a/tests/Database/ConnectionTest.php b/tests/Database/ConnectionTest.php index bf85cd2..378a0d2 100644 --- a/tests/Database/ConnectionTest.php +++ b/tests/Database/ConnectionTest.php @@ -122,3 +122,11 @@ // // Schema::hasTable('dummy'); //})->throws(QueryException::class); + + +test('threadCount', function () { + $connection = $this->connection; + + expect($connection->threadCount())->toBeInt(); + expect($connection->threadCount())->toBeGreaterThan(-1); +}); From a642392fc103f0a4407cc2801501ce2b211680d6 Mon Sep 17 00:00:00 2001 From: Laravel Freelancer NL <36150929+LaravelFreelancerNL@users.noreply.github.com> Date: Sun, 24 Nov 2024 12:58:53 +0100 Subject: [PATCH 10/34] Added getServerVersion to connection (#174) --- src/Connection.php | 16 ++++++++++++++++ tests/Database/ConnectionTest.php | 7 +++++++ 2 files changed, 23 insertions(+) diff --git a/src/Connection.php b/src/Connection.php index cce4336..b191382 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -296,4 +296,20 @@ public function threadCount() return $this->arangoClient->monitor()->getCurrentConnections(); } + + /** + * Get the server version for the connection. + * + * @return string + */ + public function getServerVersion(): string + { + if (!$this->arangoClient) { + return ''; + } + + $rawVersion = $this->arangoClient->admin()->getVersion(); + + return $rawVersion->version; + } } diff --git a/tests/Database/ConnectionTest.php b/tests/Database/ConnectionTest.php index 378a0d2..0958b7b 100644 --- a/tests/Database/ConnectionTest.php +++ b/tests/Database/ConnectionTest.php @@ -130,3 +130,10 @@ expect($connection->threadCount())->toBeInt(); expect($connection->threadCount())->toBeGreaterThan(-1); }); + +test('getServerVersion', function () { + $connection = $this->connection; + + expect($connection->getServerVersion())->toBeString(); + expect($connection->getServerVersion())->toMatch('/^\d+\.\d+\.\d+$/i'); +}); From 381d12a74fad3950789ba064029ea7a043c0cacc Mon Sep 17 00:00:00 2001 From: Laravel Freelancer NL <36150929+LaravelFreelancerNL@users.noreply.github.com> Date: Sun, 1 Dec 2024 14:14:37 +0100 Subject: [PATCH 11/34] 166 add dropallanalyzers functionality (#176) * fix typo * chore: let arango client handle view deletion * feat: add dropAllAnalyzers to Schema Builder * feat: add graph crud to schema builder * choe: style fix * feat: Extend db:wipe with --drop-analyzers and --drop-graphs * feat: Extend migrate:fresh with --drop-analyzers and --drop-graphs * docs: updated docs on console commands * docs: updated compatibility list * feat: Added support for dropAnalyzers, dropGraphs and dropAll to TestCase --- composer.json | 2 +- docs/compatibility-list.md | 10 ++ docs/console-commands.md | 50 ++++++ docs/migrations.md | 43 ++++- src/Console/Migrations/FreshCommand.php | 94 ++++++++++ src/Console/WipeCommand.php | 119 ++++++++++++ src/Providers/CommandServiceProvider.php | 14 +- src/Providers/MigrationServiceProvider.php | 15 ++ src/Schema/Builder.php | 2 + src/Schema/Concerns/HandlesAnalyzers.php | 10 ++ src/Schema/Concerns/HandlesGraphs.php | 69 +++++++ src/Schema/Concerns/HandlesViews.php | 8 +- .../CanConfigureMigrationCommands.php | 59 ++++++ src/Testing/DatabaseMigrations.php | 28 +-- src/Testing/DatabaseTruncation.php | 27 +-- src/Testing/RefreshDatabase.php | 8 + tests/Console/MigrateFreshCommandTest.php | 169 ++++++++++++++++++ tests/Console/WipeCommandTest.php | 152 ++++++++++++++++ tests/Database/ConnectionTest.php | 1 - tests/Schema/AnalyzerTest.php | 18 ++ tests/Schema/GraphTest.php | 98 ++++++++++ tests/TestCase.php | 2 +- tests/Testing/DatabaseTruncationTest.php | 1 - 23 files changed, 946 insertions(+), 53 deletions(-) create mode 100644 docs/console-commands.md create mode 100644 src/Console/Migrations/FreshCommand.php create mode 100644 src/Console/WipeCommand.php create mode 100644 src/Schema/Concerns/HandlesGraphs.php create mode 100644 src/Testing/Concerns/CanConfigureMigrationCommands.php create mode 100644 tests/Console/WipeCommandTest.php create mode 100644 tests/Schema/GraphTest.php diff --git a/composer.json b/composer.json index ba85411..ac78cdd 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "php": "^8.2", "ext-json": "*", "composer/composer": "^2.7.0", - "laravel-freelancer-nl/arangodb-php-client": "^2.3.0", + "laravel-freelancer-nl/arangodb-php-client": "^2.7.0", "laravel-freelancer-nl/fluentaql": "^2.0", "laravel/framework": "^11.0", "spatie/laravel-data": "^4.4.0", diff --git a/docs/compatibility-list.md b/docs/compatibility-list.md index aa39b30..5cedd44 100644 --- a/docs/compatibility-list.md +++ b/docs/compatibility-list.md @@ -207,6 +207,16 @@ castAsJson (dummy method) ## Database connection escape +## Console commands +The following database related console commands are compatible with vanilla Laravel: + +db:monitor / db:seed / db:wipe +make:migrate / migrate / +migrate:fresh / migrate:install / migrate:refresh / migrate:reset / migrate:rollback / migrate:status + +### Incompatible console commands +db:show / db:table + ## Known incompatibilities Not all features can be made compatible. Known issues are listed below: diff --git a/docs/console-commands.md b/docs/console-commands.md new file mode 100644 index 0000000..4b8dbf2 --- /dev/null +++ b/docs/console-commands.md @@ -0,0 +1,50 @@ +# Console commands + +## db:wipe +db:wipe lets you clear all tables within current database. +In addition, you have the option to clear the following in ArangoDB: + +* --drop-analyzers: clear all custom analyzers, predefined system analyzers remain +* --drop-views: drop all views +* --drop-graphs: drop all named graphs +* --drop-all: drop all of the above: tables, analyzers, views and graphs + +## Migrations +_**Migrations for ArangoDB use a different Schema blueprint. Therefore, you either need to run the convert:migrations +command first, or convert them manually**_ + +The other migration commands work as expected with a few added features detailed below. + +### convert:migrations +`php artisan convert:migrations` converts all available migrations to their ArangoDB counterpart. +After this you use migrations as normal. + +If you are using a multi database setup with migrations for each, you'll want it convert them manually. +Which means importing the Blueprint and Facade from this package. + +Replace: +``` +Illuminate\Database\Schema\Blueprint; +Illuminate\Support\Facades\Schema; +``` +for: +``` +use LaravelFreelancerNL\Aranguent\Schema\Blueprint; +use LaravelFreelancerNL\Aranguent\Facades\Schema; +``` + +### migrate:fresh +`php artisan migrate:fresh` uses db:wipe under the hood and can use the same --drop-{feature} options. + +## make:migration +`php artisan make:migration` gives you the additional option to create an edge table. This presets +the proper collection type within the migration file. + +# make:model +`php artisan make:model` uses stubs with some preset docblock properties. In addition, you can +select the following stubs: + +``` + --edge-pivot The generated model uses a custom intermediate edge-collection model for ArangoDB + --edge-morph-pivot The generated model uses a custom polymorphic intermediate edge-collection model for ArangoDB + ``` \ No newline at end of file diff --git a/docs/migrations.md b/docs/migrations.md index 6d25a5f..deccb1a 100644 --- a/docs/migrations.md +++ b/docs/migrations.md @@ -101,4 +101,45 @@ Schema::replaceAnalyzer($name, $type, $properties, $features); ### Delete Analyzer ```php Schema::dropAnalyzer($name); -``` \ No newline at end of file +``` + +## Named Graphs +Named graphs are predefined managed graphs which feature integrity checks +compared to anonymous graphs. + +You can perform basic CRUD operations through the schema builder to handle named graphs. + +### Create a new graph +```php +Schema::createGraph($name, $properties, $waitForSync); +``` + +### Check for graph existence +```php +Schema::hasGraph($name); +``` + +### Get data of existing graph +```php +Schema::getGraph($name); +``` + +### Get all graphs +```php +Schema::getAllGraphs(); +``` + +### Delete a graph +```php +Schema::dropGraph($name); +``` + +### Delete a graph if it exists +```php +Schema::dropGraphIfExists($name); +``` + +### Delete all graphs +```php +Schema::dropAllGraphs(); +``` \ No newline at end of file diff --git a/src/Console/Migrations/FreshCommand.php b/src/Console/Migrations/FreshCommand.php new file mode 100644 index 0000000..88d096a --- /dev/null +++ b/src/Console/Migrations/FreshCommand.php @@ -0,0 +1,94 @@ +isProhibited() || + ! $this->confirmToProceed()) { + return Command::FAILURE; + } + + $database = $this->input->getOption('database'); + + $this->migrator->usingConnection($database, function () use ($database) { + if ($this->migrator->repositoryExists()) { + $this->newLine(); + + $this->components->task('Dropping all tables', fn() => $this->callSilent('db:wipe', array_filter([ + '--database' => $database, + '--drop-all' => $this->option('drop-all'), + '--drop-analyzers' => $this->option('drop-analyzers'), + '--drop-graphs' => $this->option('drop-graphs'), + '--drop-views' => $this->option('drop-views'), + '--drop-types' => $this->option('drop-types'), + '--force' => true, + ])) == 0); + } + }); + + $this->newLine(); + + $this->call('migrate', array_filter([ + '--database' => $database, + '--path' => $this->input->getOption('path'), + '--realpath' => $this->input->getOption('realpath'), + '--schema-path' => $this->input->getOption('schema-path'), + '--force' => true, + '--step' => $this->option('step'), + ])); + + if ($this->laravel->bound(Dispatcher::class)) { + /** @phpstan-ignore offsetAccess.nonOffsetAccessible */ + $this->laravel[Dispatcher::class]->dispatch( + new DatabaseRefreshed($database, $this->needsSeeding()), + ); + } + + if ($this->needsSeeding()) { + $this->runSeeder($database); + } + + return 0; + } + + /** + * Get the console command options. + * + * @return array> + */ + protected function getOptions() + { + return [ + ['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use'], + ['drop-all', null, InputOption::VALUE_NONE, 'Drop all tables, views, custom analyzers and named graphs (ArangoDB only)'], + ['drop-analyzers', null, InputOption::VALUE_NONE, 'Drop all tables and custom analyzers (ArangoDB only)'], + ['drop-graphs', null, InputOption::VALUE_NONE, 'Drop all tables and named graphs (ArangoDB only)'], + ['drop-views', null, InputOption::VALUE_NONE, 'Drop all tables and views (ArangoDB only)'], + ['drop-types', null, InputOption::VALUE_NONE, 'Drop all tables and types (Postgres only)'], + ['force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production'], + ['path', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'The path(s) to the migrations files to be executed'], + ['realpath', null, InputOption::VALUE_NONE, 'Indicate any provided migration file paths are pre-resolved absolute paths'], + ['schema-path', null, InputOption::VALUE_OPTIONAL, 'The path to a schema dump file'], + ['seed', null, InputOption::VALUE_NONE, 'Indicates if the seed task should be re-run'], + ['seeder', null, InputOption::VALUE_OPTIONAL, 'The class name of the root seeder'], + ['step', null, InputOption::VALUE_NONE, 'Force the migrations to be run so they can be rolled back individually'], + ]; + } +} diff --git a/src/Console/WipeCommand.php b/src/Console/WipeCommand.php new file mode 100644 index 0000000..f616064 --- /dev/null +++ b/src/Console/WipeCommand.php @@ -0,0 +1,119 @@ +isProhibited() || + ! $this->confirmToProceed()) { + return Command::FAILURE; + } + + $database = $this->input->getOption('database'); + + $this->handleDrops($database); + + return 0; + } + + /** + * @param string $database + * @return void + */ + protected function handleDrops($database) + { + if ($this->option('drop-graphs') || $this->option('drop-all')) { + $this->dropAllGraphs($database); + + $this->components->info('Dropped all named graphs successfully.'); + } + + if ($this->option('drop-views') || $this->option('drop-all')) { + $this->dropAllViews($database); + + $this->components->info('Dropped all views successfully.'); + } + + $this->dropAllTables($database); + + $this->components->info('Dropped all tables successfully.'); + + if ($this->option('drop-types')) { + $this->dropAllTypes($database); + + $this->components->info('Dropped all types successfully.'); + } + + if ($this->option('drop-analyzers') || $this->option('drop-all')) { + $this->dropAllAnalyzers($database); + + $this->components->info('Dropped all analyzers successfully.'); + } + } + + /** + * Drop all of the database analyzers. + * + * @param null|string $database + * @return void + */ + protected function dropAllAnalyzers($database) + { + /** @phpstan-ignore offsetAccess.nonOffsetAccessible */ + $this->laravel['db']->connection($database) + ->getSchemaBuilder() + ->dropAllAnalyzers(); + } + + /** + * Drop all of the database analyzers. + * + * @param null|string $database + * @return void + */ + protected function dropAllGraphs($database) + { + /** @phpstan-ignore offsetAccess.nonOffsetAccessible */ + $this->laravel['db']->connection($database) + ->getSchemaBuilder() + ->dropAllGraphs(); + } + + /** + * Get the console command options. + * + * @return array> + */ + protected function getOptions() + { + return [ + ['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use'], + ['drop-all', null, InputOption::VALUE_NONE, 'Drop all tables, views, custom analyzers and named graphs (ArangoDB only)'], + ['drop-analyzers', null, InputOption::VALUE_NONE, 'Drop all tables and custom analyzers (ArangoDB only)'], + ['drop-graphs', null, InputOption::VALUE_NONE, 'Drop all tables and named graphs (ArangoDB only)'], + ['drop-views', null, InputOption::VALUE_NONE, 'Drop all tables and views (ArangoDB only)'], + ['drop-types', null, InputOption::VALUE_NONE, 'Drop all tables and types (Postgres only)'], + ['force', null, InputOption::VALUE_NONE, 'Force the operation to run when in production'], + ]; + } +} diff --git a/src/Providers/CommandServiceProvider.php b/src/Providers/CommandServiceProvider.php index d73f802..ffef389 100644 --- a/src/Providers/CommandServiceProvider.php +++ b/src/Providers/CommandServiceProvider.php @@ -4,6 +4,7 @@ namespace LaravelFreelancerNL\Aranguent\Providers; +use LaravelFreelancerNL\Aranguent\Console\WipeCommand; use LaravelFreelancerNL\Aranguent\Console\DbCommand; use Illuminate\Database\Console\DbCommand as IlluminateDbCommand; use LaravelFreelancerNL\Aranguent\Console\ModelMakeCommand; @@ -21,6 +22,7 @@ class CommandServiceProvider extends ServiceProvider protected $commands = [ 'ModelMake' => ModelMakeCommand::class, 'Db' => DbCommand::class, + 'DbWipe' => WipeCommand::class, ]; @@ -39,11 +41,19 @@ public function register() * * @param string[] $commands * @return void + * + * @SuppressWarnings(PHPMD.ElseExpression) */ protected function registerCommands(array $commands) { - foreach (array_keys($commands) as $command) { - $this->{"register{$command}Command"}(); + foreach ($commands as $commandName => $command) { + $method = "register{$commandName}Command"; + + if (method_exists($this, $method)) { + $this->{$method}(); + } else { + $this->app->singleton($command); + } } $this->commands(array_values($commands)); diff --git a/src/Providers/MigrationServiceProvider.php b/src/Providers/MigrationServiceProvider.php index 01f2920..1f65814 100644 --- a/src/Providers/MigrationServiceProvider.php +++ b/src/Providers/MigrationServiceProvider.php @@ -7,6 +7,7 @@ use Illuminate\Database\Migrations\Migrator; use Illuminate\Database\MigrationServiceProvider as IlluminateMigrationServiceProvider; use LaravelFreelancerNL\Aranguent\Console\Concerns\ArangoCommands; +use LaravelFreelancerNL\Aranguent\Console\Migrations\FreshCommand; use LaravelFreelancerNL\Aranguent\Console\Migrations\MigrationsConvertCommand; use LaravelFreelancerNL\Aranguent\Console\Migrations\MigrateMakeCommand; use LaravelFreelancerNL\Aranguent\Console\Migrations\MigrateInstallCommand; @@ -28,6 +29,7 @@ class MigrationServiceProvider extends IlluminateMigrationServiceProvider 'Repository' => 'migration.repository', 'MigrateMake' => 'migrate.make', 'MigrateInstall' => MigrateInstallCommand::class, + 'MigrateFresh' => FreshCommand::class, ]; /** @@ -51,6 +53,7 @@ public function boot(): void $this->commands([ MigrateMakeCommand::class, MigrationsConvertCommand::class, + FreshCommand::class, ]); } } @@ -153,6 +156,18 @@ protected function registerMigrationsConvertCommand(): void }); } + /** + * Register the command. + * + * @return void + */ + protected function registerMigrateFreshCommand() + { + $this->app->singleton(FreshCommand::class, function ($app) { + return new FreshCommand($app['migrator']); + }); + } + /** * @return string[] */ diff --git a/src/Schema/Builder.php b/src/Schema/Builder.php index 13ceaf9..74de5ba 100644 --- a/src/Schema/Builder.php +++ b/src/Schema/Builder.php @@ -13,6 +13,7 @@ use LaravelFreelancerNL\Aranguent\Exceptions\QueryException; use LaravelFreelancerNL\Aranguent\Schema\Concerns\HandlesAnalyzers; use LaravelFreelancerNL\Aranguent\Schema\Concerns\HandlesIndexNaming; +use LaravelFreelancerNL\Aranguent\Schema\Concerns\HandlesGraphs; use LaravelFreelancerNL\Aranguent\Schema\Concerns\HandlesViews; use LaravelFreelancerNL\Aranguent\Schema\Concerns\UsesBlueprints; @@ -20,6 +21,7 @@ class Builder extends \Illuminate\Database\Schema\Builder { use HandlesAnalyzers; use HandlesIndexNaming; + use HandlesGraphs; use HandlesViews; use UsesBlueprints; diff --git a/src/Schema/Concerns/HandlesAnalyzers.php b/src/Schema/Concerns/HandlesAnalyzers.php index 687cf28..f086991 100644 --- a/src/Schema/Concerns/HandlesAnalyzers.php +++ b/src/Schema/Concerns/HandlesAnalyzers.php @@ -86,4 +86,14 @@ public function dropAnalyzerIfExists(string $name): bool return true; } + + /** + * Drop all custom analyzers from the schema. + * + * @throws ArangoException + */ + public function dropAllAnalyzers(): void + { + $this->schemaManager->deleteAllAnalyzers(); + } } diff --git a/src/Schema/Concerns/HandlesGraphs.php b/src/Schema/Concerns/HandlesGraphs.php new file mode 100644 index 0000000..190d600 --- /dev/null +++ b/src/Schema/Concerns/HandlesGraphs.php @@ -0,0 +1,69 @@ + $properties + * @throws ArangoException + * + * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + */ + public function createGraph(string $name, array $properties = [], bool $waitForSync = false) + { + $this->schemaManager->createGraph($name, $properties, $waitForSync); + } + + public function hasGraph(string $name): bool + { + return $this->handleExceptionsAsQueryExceptions(function () use ($name) { + return $this->schemaManager->hasGraph($name); + }); + } + + /** + * @throws ArangoException + */ + public function getGraph(string $name): \stdClass + { + return $this->schemaManager->getGraph($name); + } + + /** + * @throws ArangoException + */ + public function getAllGraphs(): array + { + return $this->schemaManager->getGraphs(); + } + + /** + * @throws ArangoException + */ + public function dropGraph(string $name) + { + $this->schemaManager->deleteGraph($name); + } + + /** + * @throws ArangoException + */ + public function dropGraphIfExists(string $name): bool + { + if ($this->hasGraph($name)) { + $this->schemaManager->deleteGraph($name); + } + + return true; + } + + public function dropAllGraphs(): void + { + $this->schemaManager->deleteAllGraphs(); + } +} diff --git a/src/Schema/Concerns/HandlesViews.php b/src/Schema/Concerns/HandlesViews.php index b3c67aa..5b09842 100644 --- a/src/Schema/Concerns/HandlesViews.php +++ b/src/Schema/Concerns/HandlesViews.php @@ -85,13 +85,9 @@ public function dropViewIfExists(string $name): bool * Drop all views from the schema. * * @throws ArangoException - */ + */ public function dropAllViews(): void { - $views = $this->schemaManager->getViews(); - - foreach ($views as $view) { - $this->schemaManager->deleteView($view->name); - } + $this->schemaManager->deleteAllViews(); } } diff --git a/src/Testing/Concerns/CanConfigureMigrationCommands.php b/src/Testing/Concerns/CanConfigureMigrationCommands.php new file mode 100644 index 0000000..40d46b4 --- /dev/null +++ b/src/Testing/Concerns/CanConfigureMigrationCommands.php @@ -0,0 +1,59 @@ +|string> + */ + protected function setMigrationPaths() + { + $migrationSettings = []; + + if (property_exists($this, 'realPath')) { + $migrationSettings['--realpath'] = $this->realPath ?? false; + } + + if (property_exists($this, 'migrationPaths')) { + $migrationSettings['--path'] = $this->migrationPaths; + } + + return $migrationSettings; + } + + /** + * Determine if custom analyzers should be dropped when refreshing the database. + * + * @return bool + */ + protected function shouldDropAnalyzers() + { + return property_exists($this, 'dropAnalyzers') ? $this->dropAnalyzers : false; + } + + /** + * Determine if graphs should be dropped when refreshing the database. + * + * @return bool + */ + protected function shouldDropGraphs() + { + return property_exists($this, 'dropGraphs') ? $this->dropGraphs : false; + } + + + /** + * Determine if all analyzers, graphs and views should be dropped when refreshing the database. + * + * @return bool + */ + protected function shouldDropAll() + { + return property_exists($this, 'dropAll') ? $this->dropAll : false; + } +} diff --git a/src/Testing/DatabaseMigrations.php b/src/Testing/DatabaseMigrations.php index aaedcac..d5cf3f3 100644 --- a/src/Testing/DatabaseMigrations.php +++ b/src/Testing/DatabaseMigrations.php @@ -5,14 +5,19 @@ namespace LaravelFreelancerNL\Aranguent\Testing; use Illuminate\Foundation\Testing\DatabaseMigrations as IlluminateDatabaseMigrations; +use LaravelFreelancerNL\Aranguent\Testing\Concerns\CanConfigureMigrationCommands; trait DatabaseMigrations { use IlluminateDatabaseMigrations; + use CanConfigureMigrationCommands; + /** * The parameters that should be used when running "migrate:fresh". * + * Duplicate code because CanConfigureMigrationCommands has a conflict otherwise. + * * @return array */ protected function migrateFreshUsing() @@ -21,8 +26,11 @@ protected function migrateFreshUsing() $results = array_merge( [ + '--drop-analyzers' => $this->shouldDropAnalyzers(), + '--drop-graphs' => $this->shouldDropGraphs(), '--drop-views' => $this->shouldDropViews(), '--drop-types' => $this->shouldDropTypes(), + '--drop-all' => $this->shouldDropAll(), ], $seeder ? ['--seeder' => $seeder] : ['--seed' => $this->shouldSeed()], $this->setMigrationPaths(), @@ -30,24 +38,4 @@ protected function migrateFreshUsing() return $results; } - - /** - * Determine if types should be dropped when refreshing the database. - * - * @return array|string> - */ - protected function setMigrationPaths() - { - $migrationSettings = []; - - if (property_exists($this, 'realPath')) { - $migrationSettings['--realpath'] = $this->realPath ?? false; - } - - if (property_exists($this, 'migrationPaths')) { - $migrationSettings['--path'] = $this->migrationPaths; - } - - return $migrationSettings; - } } diff --git a/src/Testing/DatabaseTruncation.php b/src/Testing/DatabaseTruncation.php index 7edf921..28270dd 100644 --- a/src/Testing/DatabaseTruncation.php +++ b/src/Testing/DatabaseTruncation.php @@ -5,14 +5,18 @@ namespace LaravelFreelancerNL\Aranguent\Testing; use Illuminate\Foundation\Testing\DatabaseTruncation as IlluminateDatabaseTruncation; +use LaravelFreelancerNL\Aranguent\Testing\Concerns\CanConfigureMigrationCommands; trait DatabaseTruncation { use IlluminateDatabaseTruncation; + use CanConfigureMigrationCommands; /** * The parameters that should be used when running "migrate:fresh". * + * Duplicate code because CanConfigureMigrationCommands has a conflict otherwise. + * * @return array */ protected function migrateFreshUsing() @@ -21,8 +25,11 @@ protected function migrateFreshUsing() $results = array_merge( [ + '--drop-analyzers' => $this->shouldDropAnalyzers(), + '--drop-graphs' => $this->shouldDropGraphs(), '--drop-views' => $this->shouldDropViews(), '--drop-types' => $this->shouldDropTypes(), + '--drop-all' => $this->shouldDropAll(), ], $seeder ? ['--seeder' => $seeder] : ['--seed' => $this->shouldSeed()], $this->setMigrationPaths(), @@ -30,24 +37,4 @@ protected function migrateFreshUsing() return $results; } - - /** - * Determine if types should be dropped when refreshing the database. - * - * @return array|string> - */ - protected function setMigrationPaths() - { - $migrationSettings = []; - - if (property_exists($this, 'realPath')) { - $migrationSettings['--realpath'] = $this->realPath ?? false; - } - - if (property_exists($this, 'migrationPaths')) { - $migrationSettings['--path'] = $this->migrationPaths; - } - - return $migrationSettings; - } } diff --git a/src/Testing/RefreshDatabase.php b/src/Testing/RefreshDatabase.php index 282a5dc..5d6e3fd 100644 --- a/src/Testing/RefreshDatabase.php +++ b/src/Testing/RefreshDatabase.php @@ -6,11 +6,13 @@ use Illuminate\Foundation\Testing\DatabaseTransactionsManager; use Illuminate\Foundation\Testing\RefreshDatabase as IlluminateRefreshDatabase; +use LaravelFreelancerNL\Aranguent\Testing\Concerns\CanConfigureMigrationCommands; use LaravelFreelancerNL\Aranguent\Testing\Concerns\PreparesTestingTransactions; trait RefreshDatabase { use PreparesTestingTransactions; + use CanConfigureMigrationCommands; use IlluminateRefreshDatabase; /** @@ -51,9 +53,12 @@ public function beginDatabaseTransaction() }); } + /** * The parameters that should be used when running "migrate:fresh". * + * Duplicate code because CanConfigureMigrationCommands has a conflict otherwise. + * * @return array */ protected function migrateFreshUsing() @@ -62,8 +67,11 @@ protected function migrateFreshUsing() $results = array_merge( [ + '--drop-analyzers' => $this->shouldDropAnalyzers(), + '--drop-graphs' => $this->shouldDropGraphs(), '--drop-views' => $this->shouldDropViews(), '--drop-types' => $this->shouldDropTypes(), + '--drop-all' => $this->shouldDropAll(), ], $seeder ? ['--seeder' => $seeder] : ['--seed' => $this->shouldSeed()], $this->setMigrationPaths(), diff --git a/tests/Console/MigrateFreshCommandTest.php b/tests/Console/MigrateFreshCommandTest.php index 7dd30ef..25d4c2c 100644 --- a/tests/Console/MigrateFreshCommandTest.php +++ b/tests/Console/MigrateFreshCommandTest.php @@ -1,5 +1,7 @@ 'none', ])->assertExitCode(0); })->throws(InvalidArgumentException::class); + +test('migrate:fresh --drop-views', function () { + $path = [ + realpath(__DIR__ . '/../../TestSetup/Database/Migrations'), + ]; + + if (!$this->schemaManager->hasView('dropViewTest')) { + Schema::createView('dropViewTest', []); + } + + $this->artisan('migrate:fresh', [ + '--path' => [ + database_path('migrations'), + realpath(__DIR__ . '/../../TestSetup/Database/Migrations'), + realpath(__DIR__ . '/../../vendor/orchestra/testbench-core/laravel/migrations/'), + ], + '--realpath' => true, + '--seed' => true, + '--seeder' => DatabaseSeeder::class, + '--drop-views' => true, + + ])->assertExitCode(0); + + $views = $this->schemaManager->getViews(); + expect(count($views))->toBe(2); +}); + +test('migrate:fresh --drop-analyzers', function () { + $path = [ + realpath(__DIR__ . '/../../TestSetup/Database/Migrations'), + ]; + + if (!$this->schemaManager->hasAnalyzer('dropMyAnalyzer')) { + Schema::createAnalyzer( + 'dropMyAnalyzer', + 'identity', + ); + } + + $this->artisan('migrate:fresh', [ + '--path' => [ + database_path('migrations'), + realpath(__DIR__ . '/../../TestSetup/Database/Migrations'), + realpath(__DIR__ . '/../../vendor/orchestra/testbench-core/laravel/migrations/'), + ], + '--realpath' => true, + '--seed' => true, + '--seeder' => DatabaseSeeder::class, + '--drop-analyzers' => true, + + ])->assertExitCode(0); + + $analyzers = $this->schemaManager->getAnalyzers(); + expect(count($analyzers))->toBe(13); +}); + +test('migrate:fresh --drop-graphs', function () { + $path = [ + realpath(__DIR__ . '/../../TestSetup/Database/Migrations'), + ]; + + if (!$this->schemaManager->hasGraph('dropMyGraph')) { + Schema::createGraph( + 'dropMyGraph', + [ + 'edgeDefinitions' => [ + [ + 'collection' => 'children', + 'from' => ['characters'], + 'to' => ['characters'], + ], + ], + ], + true, + ); + } + + $this->artisan('migrate:fresh', [ + '--path' => [ + database_path('migrations'), + realpath(__DIR__ . '/../../TestSetup/Database/Migrations'), + realpath(__DIR__ . '/../../vendor/orchestra/testbench-core/laravel/migrations/'), + ], + '--realpath' => true, + '--seed' => true, + '--seeder' => DatabaseSeeder::class, + '--drop-graphs' => true, + + ])->assertExitCode(0); + + $graphs = $this->schemaManager->getGraphs(); + expect(count($graphs))->toBe(0); +}); + +test('migrate:fresh --drop-all', function () { + $path = [ + realpath(__DIR__ . '/../../TestSetup/Database/Migrations'), + ]; + + if (!$this->schemaManager->hasAnalyzer('dropMyAnalyzer')) { + Schema::createAnalyzer( + 'dropMyAnalyzer', + 'identity', + ); + } + if (!$this->schemaManager->hasGraph('dropMyGraph')) { + Schema::createGraph( + 'dropMyGraph', + [ + 'edgeDefinitions' => [ + [ + 'collection' => 'children', + 'from' => ['characters'], + 'to' => ['characters'], + ], + ], + ], + true, + ); + } + + if (!$this->schemaManager->hasView('dropViewTest')) { + Schema::createView('dropViewTest', []); + } + + $this->artisan('migrate:fresh', [ + '--path' => [ + database_path('migrations'), + realpath(__DIR__ . '/../../TestSetup/Database/Migrations'), + realpath(__DIR__ . '/../../vendor/orchestra/testbench-core/laravel/migrations/'), + ], + '--realpath' => true, + '--seed' => true, + '--seeder' => DatabaseSeeder::class, + '--drop-all' => true, + + ])->assertExitCode(0); + + + $analyzers = $this->schemaManager->getAnalyzers(); + expect(count($analyzers))->toBe(13); + + $graphs = $this->schemaManager->getGraphs(); + expect(count($graphs))->toBe(0); + + $views = $this->schemaManager->getViews(); + expect(count($views))->toBe(2); +}); + +test('migrate:fresh --drop-types', function () { + $path = [ + realpath(__DIR__ . '/../../TestSetup/Database/Migrations'), + ]; + + $this->artisan('migrate:fresh', [ + '--path' => [ + database_path('migrations'), + realpath(__DIR__ . '/../../TestSetup/Database/Migrations'), + realpath(__DIR__ . '/../../vendor/orchestra/testbench-core/laravel/migrations/'), + ], + '--realpath' => true, + '--seed' => true, + '--seeder' => DatabaseSeeder::class, + '--drop-types' => true, + + ])->assertExitCode(0); +})->throws('This database driver does not support dropping all types.'); diff --git a/tests/Console/WipeCommandTest.php b/tests/Console/WipeCommandTest.php new file mode 100644 index 0000000..f41add2 --- /dev/null +++ b/tests/Console/WipeCommandTest.php @@ -0,0 +1,152 @@ +schemaManager = $this->connection->getArangoClient()->schema(); +}); + +afterEach(function () { + $path = [ + realpath(__DIR__ . '/../../TestSetup/Database/Migrations'), + ]; + + $this->artisan('migrate:fresh', [ + '--path' => [ + database_path('migrations'), + realpath(__DIR__ . '/../../TestSetup/Database/Migrations'), + realpath(__DIR__ . '/../../vendor/orchestra/testbench-core/laravel/migrations/'), + ], + '--realpath' => true, + '--seed' => true, + '--seeder' => DatabaseSeeder::class, + '--drop-all' => true, + ]); +}); + +test('db:wipe', function () { + $this->artisan('db:wipe')->assertExitCode(0); + + $views = $this->schemaManager->getViews(); + expect(count($views))->toBe(2); +}); + +test('db:wipe --database=arangodb', function () { + $this->artisan('db:wipe', [ + '--database' => 'arangodb', + ])->assertExitCode(0); + + $views = $this->schemaManager->getViews(); + expect(count($views))->toBe(2); +}); + +test('migrate:fresh --database=none', function () { + $this->artisan('db:wipe', [ + '--database' => 'none', + ])->assertExitCode(0); + + $views = $this->schemaManager->getViews(); + expect(count($views))->toBe(2); +})->throws(InvalidArgumentException::class); + +test('db:wipe --drop-views', function () { + $this->artisan('db:wipe', [ + '--drop-views' => true, + ])->assertExitCode(0); + + $views = $this->schemaManager->getViews(); + expect(count($views))->toBe(0); +}); + +test('db:wipe --drop-analyzers', function () { + $schemaManager = $this->connection->getArangoClient()->schema(); + if (!$schemaManager->hasAnalyzer('dropMyAnalyzer')) { + Schema::createAnalyzer( + 'dropMyAnalyzer', + 'identity', + ); + } + + $this->artisan('db:wipe', [ + '--drop-analyzers' => true, + ])->assertExitCode(0); + + $analyzers = $this->schemaManager->getAnalyzers(); + expect(count($analyzers))->toBe(13); +}); + +test('db:wipe --drop-graphs', function () { + $schemaManager = $this->connection->getArangoClient()->schema(); + if (!$schemaManager->hasGraph('dropMyGraph')) { + Schema::createGraph( + 'dropMyGraph', + [ + 'edgeDefinitions' => [ + [ + 'collection' => 'children', + 'from' => ['characters'], + 'to' => ['characters'], + ], + ], + ], + true, + ); + } + + + $this->artisan('db:wipe', [ + '--drop-graphs' => true, + ])->assertExitCode(0); + + $graphs = $this->schemaManager->getGraphs(); + expect(count($graphs))->toBe(0); +}); + +test('db:wipe --drop-all', function () { + $schemaManager = $this->connection->getArangoClient()->schema(); + if (!$schemaManager->hasAnalyzer('dropMyAnalyzer')) { + Schema::createAnalyzer( + 'dropMyAnalyzer', + 'identity', + ); + } + + if (!$schemaManager->hasGraph('dropMyGraph')) { + Schema::createGraph( + 'dropMyGraph', + [ + 'edgeDefinitions' => [ + [ + 'collection' => 'children', + 'from' => ['characters'], + 'to' => ['characters'], + ], + ], + ], + true, + ); + } + + if (!$schemaManager->hasView('dropViewTest')) { + Schema::createView('dropViewTest', []); + } + + $this->artisan('db:wipe', [ + '--drop-all' => true, + ])->assertExitCode(0); + + $analyzers = $this->schemaManager->getAnalyzers(); + expect(count($analyzers))->toBe(13); + + $graphs = $this->schemaManager->getGraphs(); + expect(count($graphs))->toBe(0); + + $views = $this->schemaManager->getViews(); + expect(count($views))->toBe(0); +}); + +test('db:wipe --drop-types', function () { + $this->artisan('db:wipe', [ + '--drop-types' => true, + ])->assertExitCode(0); +})->throws('This database driver does not support dropping all types.'); diff --git a/tests/Database/ConnectionTest.php b/tests/Database/ConnectionTest.php index 0958b7b..a5dab05 100644 --- a/tests/Database/ConnectionTest.php +++ b/tests/Database/ConnectionTest.php @@ -123,7 +123,6 @@ // Schema::hasTable('dummy'); //})->throws(QueryException::class); - test('threadCount', function () { $connection = $this->connection; diff --git a/tests/Schema/AnalyzerTest.php b/tests/Schema/AnalyzerTest.php index e284a12..bdcf8f1 100644 --- a/tests/Schema/AnalyzerTest.php +++ b/tests/Schema/AnalyzerTest.php @@ -63,3 +63,21 @@ Schema::dropAnalyzerIfExists('none-existing-analyzer'); }); + +test('dropAllAnalyzers', function () { + $schemaManager = $this->connection->getArangoClient()->schema(); + + $initialAnalyzers = Schema::getAllAnalyzers(); + + Schema::createAnalyzer('myAnalyzer1', 'identity'); + Schema::createAnalyzer('myAnalyzer2', 'identity'); + + $totalAnalyzers = Schema::getAllAnalyzers(); + + Schema::dropAllAnalyzers(); + + $endAnalyzers = Schema::getAllAnalyzers(); + + expect(count($initialAnalyzers))->toBe(count($endAnalyzers)); + expect(count($initialAnalyzers))->toBe(count($totalAnalyzers) - 2); +}); diff --git a/tests/Schema/GraphTest.php b/tests/Schema/GraphTest.php new file mode 100644 index 0000000..0de0a2c --- /dev/null +++ b/tests/Schema/GraphTest.php @@ -0,0 +1,98 @@ + [ + [ + 'collection' => 'children', + 'from' => ['characters'], + 'to' => ['characters'], + ], + ], + ], + true, + ); +} + +test('graph CRUD', function () { + $schemaManager = $this->connection->getArangoClient()->schema(); + if (!$schemaManager->hasGraph('myGraph')) { + createGraph(); + } + + $graphExists = $schemaManager->hasGraph('myGraph'); + expect($graphExists)->toBeTrue(); + + $graph = $schemaManager->getGraph('myGraph'); + expect($graph->name)->toEqual('myGraph'); + + $schemaManager->deleteGraph('myGraph'); + $graphExists = $schemaManager->hasGraph('myGraph'); + expect($graphExists)->toBeFalse(); +}); + +test('getAllGraphs', function () { + $schemaManager = $this->connection->getArangoClient()->schema(); + + $graphs = Schema::getAllGraphs(); + expect($graphs)->toHaveCount(0); + + if (!$schemaManager->hasGraph('myGraph')) { + createGraph(); + } + + $graphs = Schema::getAllGraphs(); + expect($graphs)->toHaveCount(1); +}); + +test('dropGraph', function () { + $schemaManager = $this->connection->getArangoClient()->schema(); + if (!$schemaManager->hasGraph('myGraph')) { + createGraph(); + } + + Schema::dropGraph('myGraph'); + + $schemaManager->getGraph('myGraph'); +})->throws(ArangoException::class); + +test('dropGraphIfExists true', function () { + $schemaManager = $this->connection->getArangoClient()->schema(); + if (!$schemaManager->hasAnalyzer('myGraph')) { + createGraph(); + } + Schema::dropGraphIfExists('myGraph'); + + $schemaManager->getGraph('myGraph'); +})->throws(ArangoException::class); + +test('dropGraphIfExists false', function () { + $schemaManager = $this->connection->getArangoClient()->schema(); + + Schema::dropGraphIfExists('none-existing-graph'); +}); + +test('dropAllGraphs', function () { + $schemaManager = $this->connection->getArangoClient()->schema(); + + $initialGraphs = Schema::getAllGraphs(); + + Schema::createGraph('myGraph1'); + Schema::createGraph('myGraph2'); + + $totalGraphs = Schema::getAllGraphs(); + + Schema::dropAllGraphs(); + + $endGraphs = Schema::getAllGraphs(); + + expect(count($initialGraphs))->toBe(count($endGraphs)); + expect(count($initialGraphs))->toBe(count($totalGraphs) - 2); +}); diff --git a/tests/TestCase.php b/tests/TestCase.php index 208e1e3..a351dde 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -26,7 +26,7 @@ class TestCase extends \Orchestra\Testbench\TestCase protected ?ConnectionInterface $connection; - protected bool $dropViews = true; + protected bool $dropAll = true; protected bool $realPath = true; diff --git a/tests/Testing/DatabaseTruncationTest.php b/tests/Testing/DatabaseTruncationTest.php index ce60ac3..b508707 100644 --- a/tests/Testing/DatabaseTruncationTest.php +++ b/tests/Testing/DatabaseTruncationTest.php @@ -14,7 +14,6 @@ expect(count($tables))->toEqual($this->tableCount); }); - test('Ensure all characters are present', function () { $characters = Character::all(); From e3594acfb26733d439fa689451a55e743e6d1a06 Mon Sep 17 00:00:00 2001 From: Laravel Freelancer NL <36150929+LaravelFreelancerNL@users.noreply.github.com> Date: Thu, 5 Dec 2024 14:17:00 +0100 Subject: [PATCH 12/34] 135 support dbtable (#178) * ci: added drop-all to the test setup * chore: added missing methods to the docblock for better autocompletion * fix: fixed confusing functionality for getTables & getAllTables * fix: Renamed getAllViews to getViews for better Laravel naming compatibility * fix: Renamed getAllAnalyzers to getAnalyzers for better Laravel naming compatibility * fix: Renamed getAllGraphs to getGraphs for better Laravel naming compatibility * docs: streamlined styling and added functions * test fixed schema method calls * chore cleared unused function * test fixed default endpoint * feat Extended db:show with arangodb functionality * ci: ensure migrations are converted --- bin/qa.sh | 2 +- bin/test.sh | 3 +- docs/console-commands.md | 9 + docs/migrations.md | 15 +- phpunit.xml | 2 +- src/Console/ShowCommand.php | 343 +++++++++++++++++++++++ src/Facades/Schema.php | 16 +- src/Providers/CommandServiceProvider.php | 2 + src/Schema/Builder.php | 19 +- src/Schema/Concerns/HandlesAnalyzers.php | 2 +- src/Schema/Concerns/HandlesGraphs.php | 2 +- src/Schema/Concerns/HandlesViews.php | 2 +- tests/Console/DbShowCommandTest.php | 95 +++++++ tests/Pest.php | 17 -- tests/Schema/AnalyzerTest.php | 10 +- tests/Schema/GraphTest.php | 12 +- tests/Schema/SchemaBuilderTest.php | 10 +- tests/Schema/TableTest.php | 4 +- tests/Schema/ViewTest.php | 4 +- tests/Testing/DatabaseTruncationTest.php | 2 +- 20 files changed, 518 insertions(+), 53 deletions(-) create mode 100644 src/Console/ShowCommand.php create mode 100644 tests/Console/DbShowCommandTest.php diff --git a/bin/qa.sh b/bin/qa.sh index 0b2e54f..c9dd48d 100755 --- a/bin/qa.sh +++ b/bin/qa.sh @@ -10,6 +10,6 @@ echo "Run PHPStan" echo "Test package from within phpunit" ./vendor/bin/testbench convert:migrations -./vendor/bin/testbench migrate:fresh --path=TestSetup/Database/Migrations --path=vendor/orchestra/testbench-core/laravel/migrations/ --realpath --seed +./vendor/bin/testbench migrate:fresh --drop-all --path=TestSetup/Database/Migrations --path=vendor/orchestra/testbench-core/laravel/migrations/ --realpath --seed ./vendor/bin/testbench package:test --coverage --min=80 tests diff --git a/bin/test.sh b/bin/test.sh index 7a77a27..bf5fd45 100755 --- a/bin/test.sh +++ b/bin/test.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash printf "\nRun tests\n" -./vendor/bin/testbench migrate:fresh --path=TestSetup/Database/Migrations --path=vendor/orchestra/testbench-core/laravel/migrations/ --realpath --seed +./vendor/bin/testbench convert:migrations +./vendor/bin/testbench migrate:fresh --drop-all --path=TestSetup/Database/Migrations --path=vendor/orchestra/testbench-core/laravel/migrations/ --realpath --seed ./vendor/bin/testbench package:test --coverage --min=80 tests diff --git a/docs/console-commands.md b/docs/console-commands.md index 4b8dbf2..637f365 100644 --- a/docs/console-commands.md +++ b/docs/console-commands.md @@ -9,6 +9,15 @@ In addition, you have the option to clear the following in ArangoDB: * --drop-graphs: drop all named graphs * --drop-all: drop all of the above: tables, analyzers, views and graphs +## db:show +db:show gives you an overview of the current database and its tables. +In addition to the default Laravel options, you have the following: + +* --analyzers: show a list of available analyzers +* --views: show a list of available views +* --graphs: show a list of available named graphs +* --system: include system tables in the table list + ## Migrations _**Migrations for ArangoDB use a different Schema blueprint. Therefore, you either need to run the convert:migrations command first, or convert them manually**_ diff --git a/docs/migrations.md b/docs/migrations.md index deccb1a..a5b36e2 100644 --- a/docs/migrations.md +++ b/docs/migrations.md @@ -88,21 +88,26 @@ Schema::dropView($viewName); ## Analyzers (ArangoSearch) You can create, edit or delete an ArangoDB Analyzer. -### New Analyzer +### New analyzer ```php Schema::createAnalyzer($name, $type, $properties, $features); ``` -### Replace Analyzer +### Replace analyzer ```php Schema::replaceAnalyzer($name, $type, $properties, $features); ``` -### Delete Analyzer +### Delete analyzer ```php Schema::dropAnalyzer($name); ``` +### Delete all analyzers +```php +Schema::dropAnalyzers($name); +``` + ## Named Graphs Named graphs are predefined managed graphs which feature integrity checks compared to anonymous graphs. @@ -126,7 +131,7 @@ Schema::getGraph($name); ### Get all graphs ```php -Schema::getAllGraphs(); +Schema::getGraphs(); ``` ### Delete a graph @@ -141,5 +146,5 @@ Schema::dropGraphIfExists($name); ### Delete all graphs ```php -Schema::dropAllGraphs(); +Schema::dropGraphs(); ``` \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml index 5bc8a49..105e788 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -30,7 +30,7 @@ - + diff --git a/src/Console/ShowCommand.php b/src/Console/ShowCommand.php new file mode 100644 index 0000000..b5036cd --- /dev/null +++ b/src/Console/ShowCommand.php @@ -0,0 +1,343 @@ +connection($database = $this->input->getOption('database')); + + assert($connection instanceof Connection); + + if ($connection->getDriverName() !== 'arangodb') { + return parent::handle($connections); + } + + $schema = $connection->getSchemaBuilder(); + + $fullVersion = $this->getFullVersion($connection); + + $data = [ + 'platform' => [ + 'config' => $this->getConfigFromDatabase($database), + 'server' => $fullVersion->server ?? 'arango', + 'license' => $fullVersion->license ?? 'unknown', + 'name' => $connection->getDriverTitle(), + 'connection' => $connection->getName(), + 'version' => $fullVersion->version ?? 'unknown', + 'isSystemDatabase' => $this->getDatabaseInfo($connection), + 'open_connections' => $connection->threadCount(), + ], + 'tables' => $this->tables($connection, $schema), + ]; + + $data['views'] = $this->views($connection, $schema); + + $data['analyzers'] = $this->analyzers($schema); + + $data['graphs'] = $this->graphs($schema); + + $this->display($data, $connection); + + return 0; + } + + /** + * Render the database information. + * + * @param array $data + * @param Connection|null $connection + * @return void + */ + protected function display(array $data, ?Connection $connection = null) + { + $this->option('json') ? $this->displayJson($data) : $this->displayForCli($data, $connection); + } + + + /** + * Get information regarding the tables within the database. + * + * @param \Illuminate\Database\ConnectionInterface $connection + * @param IlluminateSchemaBuilder $schema + * @return \Illuminate\Support\Collection + */ + protected function tables(ConnectionInterface $connection, $schema) + { + assert($connection instanceof Connection); + + if ($connection->getDriverName() !== 'arangodb') { + return parent::tables($connection, $schema); + } + + assert($schema instanceof SchemaBuilder); + + // Get all tables + $tables = collect( + ($this->input->getOption('system')) ? $schema->getAllTables() : $schema->getTables(), + )->sortBy('name'); + + // Get per table statistics + $tableStats = []; + foreach ($tables as $table) { + $tableStats[] = $schema->getTable($table->name); + } + + return collect($tableStats)->map(fn($table) => [ + 'table' => $table['name'], + 'size' => $table['figures']->documentsSize, + 'rows' => $this->option('counts') + ? $table['count'] + : null, + ]); + } + + /** + * Get information regarding the views within the database. + * + * @param \Illuminate\Database\ConnectionInterface $connection + * @param \Illuminate\Database\Schema\Builder $schema + * @return \Illuminate\Support\Collection + */ + protected function views(ConnectionInterface $connection, IlluminateSchemaBuilder $schema) + { + assert($connection instanceof Connection); + + if ($connection->getDriverName() !== 'arangodb') { + return parent::views($connection, $schema); + } + + return collect($schema->getViews()) + ->map(fn($view) => [ + 'name' => $view->name, + 'type' => $view->type, + ]); + } + + /** + * Get information regarding the analyzers within the database. + * + * @param SchemaBuilder $schema + * @return \Illuminate\Support\Collection + */ + protected function analyzers(SchemaBuilder $schema) + { + return collect($schema->getAnalyzers()) + ->map(fn($analyzer) => [ + 'name' => $analyzer->name, + 'type' => $analyzer->type, + ]); + } + + /** + * Get information regarding the named graphs within the database. + * + * @param SchemaBuilder $schema + * @return \Illuminate\Support\Collection + */ + protected function graphs(SchemaBuilder $schema) + { + return collect($schema->getGraphs()) + ->map(fn($graph) => [ + 'name' => $graph->name, + 'edgeDefinitions' => count($graph->edgeDefinitions), + ]); + } + + protected function getFullVersion(Connection $connection): object + { + $client = $connection->getArangoClient(); + + assert($client !== null); + + return $client->admin()->getVersion(); + } + + /** + * @throws ArangoException + */ + protected function getDatabaseInfo(Connection $connection): bool + { + $client = $connection->getArangoClient(); + + assert($client !== null); + + $info = $client->schema()->getCurrentDatabase(); + + return $info->isSystem; + } + + /** + * @param mixed $views + * @return void + */ + public function displayViews(mixed $views): void + { + if (! $this->input->getOption('views') || $views->isEmpty()) { + return; + } + + $this->components->twoColumnDetail( + 'View', + 'Type', + ); + + $views->each(fn($view) => $this->components->twoColumnDetail( + $view['name'], + $view['type'], + )); + + $this->newLine(); + } + + /** + * @param mixed $analyzers + * @return void + */ + public function displayAnalyzers(mixed $analyzers): void + { + if (! $this->input->getOption('analyzers') || $analyzers->isEmpty()) { + return; + } + + $this->components->twoColumnDetail( + 'Analyzers', + 'Type', + ); + + $analyzers->each(fn($analyzer) => $this->components->twoColumnDetail( + $analyzer['name'], + $analyzer['type'], + )); + + $this->newLine(); + } + /** + * @param mixed $graphs + * @return void + */ + public function displayGraphs(mixed $graphs): void + { + if (! $this->input->getOption('graphs') || $graphs->isEmpty()) { + return; + } + + $this->components->twoColumnDetail( + 'Graphs', + 'Edge Definitions', + ); + + $graphs->each(fn($graph) => $this->components->twoColumnDetail( + $graph['name'], + $graph['edgeDefinitions'], + )); + + $this->newLine(); + } + + /** + * Render the database information formatted for the CLI. + * + * @param array $data + * @param Connection|null $connection + * @return void + */ + protected function displayForCli(array $data, ?Connection $connection = null) + { + if ($connection && $connection->getDriverName() !== 'arangodb') { + parent::displayForCli($data); + return; + } + + $platform = $data['platform']; + $tables = $data['tables']; + $analyzers = $data['analyzers'] ?? null; + $views = $data['views'] ?? null; + $graphs = $data['graphs'] ?? null; + + $this->newLine(); + + $this->components->twoColumnDetail('ArangoDB (' . ucfirst($platform['license']) . ' Edition)', '' . $platform['version'] . ''); + $this->components->twoColumnDetail('Connection', $platform['connection']); + $this->components->twoColumnDetail('Database', Arr::get($platform['config'], 'database')); + $this->components->twoColumnDetail('Host', Arr::get($platform['config'], 'host')); + $this->components->twoColumnDetail('Port', Arr::get($platform['config'], 'port')); + $this->components->twoColumnDetail('Username', Arr::get($platform['config'], 'username')); + $this->components->twoColumnDetail('URL', Arr::get($platform['config'], 'url') ?? Arr::get($platform['config'], 'endpoint')); + $this->components->twoColumnDetail('Open Connections', $platform['open_connections']); + $this->components->twoColumnDetail('Analyzers', $analyzers->count()); + $this->components->twoColumnDetail('Views', $views->count()); + $this->components->twoColumnDetail('Named Graphs', $graphs->count()); + $this->components->twoColumnDetail('Tables', $tables->count()); + + $tableSizeSum = $tables->sum('size'); + if ($tableSizeSum) { + $this->components->twoColumnDetail('Total Size Estimate', Number::fileSize($tableSizeSum, 2)); + } + + $this->newLine(); + + if ($tables->isNotEmpty()) { + $this->components->twoColumnDetail( + 'Table', + 'Size Estimate' . ($this->option('counts') ? ' / Rows' : ''), + ); + + $tables->each(function ($table) { + $tableSize = is_null($table['size']) ? null : Number::fileSize($table['size'], 2); + + $this->components->twoColumnDetail( + $table["table"], + ($tableSize ?? '—') . ($this->option('counts') ? ' / ' . Number::format($table['rows']) . '' : ''), + ); + }); + + $this->newLine(); + } + + $this->displayViews($views); + + $this->displayAnalyzers($analyzers); + + $this->displayGraphs($graphs); + } +} diff --git a/src/Facades/Schema.php b/src/Facades/Schema.php index 3e81995..c014376 100644 --- a/src/Facades/Schema.php +++ b/src/Facades/Schema.php @@ -12,7 +12,7 @@ * Table handling: * * @method static Builder create($collection, Closure $callback, $options = []) - * @method static Builder getAllTables() + * @method static Builder getTables() * @method static Builder drop(string $collection) * @method static Builder dropIfExists(string $collection) * @method static Builder dropAllTables() @@ -21,7 +21,9 @@ * * View handling: * @method static Builder createView($name, array $properties, $type = 'arangosearch') + * @method static Builder hasView(string $name) * @method static Builder getView(string $name) + * @method static Builder getViews() * @method static Builder editView($name, array $properties) * @method static Builder renameView(string $from, string $to) * @method static Builder dropView(string $name) @@ -29,10 +31,22 @@ * * Analyzer handling: * @method static Builder createAnalyzer($name, array $properties) + * @method static Builder hasAnalyzer() * @method static Builder getAnalyzer(string $name) + * @method static Builder getAnalyzers() * @method static Builder replaceAnalyzer($name, array $properties) * @method static Builder dropAnalyzer(string $name) * @method static Builder dropAnalyzerIfExists(string $name) + * @method static Builder dropAllAnalyzers() + * + * Named Graph handling: + * @method static Builder creategraph(string $name, array $properties = [], bool $waitForSync = false) + * @method static Builder hasGraph(string $name) + * @method static Builder getGraph(string $name) + * @method static Builder getGraphs() + * @method static Builder dropGraph(string $name) + * @method static Builder dropGraphIfExists(string $name) + * @method static Builder dropAllGraphs() * * @see \LaravelFreelancerNL\Aranguent\Schema\Builder */ diff --git a/src/Providers/CommandServiceProvider.php b/src/Providers/CommandServiceProvider.php index ffef389..2dd1c2b 100644 --- a/src/Providers/CommandServiceProvider.php +++ b/src/Providers/CommandServiceProvider.php @@ -4,6 +4,7 @@ namespace LaravelFreelancerNL\Aranguent\Providers; +use LaravelFreelancerNL\Aranguent\Console\ShowCommand; use LaravelFreelancerNL\Aranguent\Console\WipeCommand; use LaravelFreelancerNL\Aranguent\Console\DbCommand; use Illuminate\Database\Console\DbCommand as IlluminateDbCommand; @@ -23,6 +24,7 @@ class CommandServiceProvider extends ServiceProvider 'ModelMake' => ModelMakeCommand::class, 'Db' => DbCommand::class, 'DbWipe' => WipeCommand::class, + 'DbShow' => ShowCommand::class, ]; diff --git a/src/Schema/Builder.php b/src/Schema/Builder.php index 74de5ba..25a5d45 100644 --- a/src/Schema/Builder.php +++ b/src/Schema/Builder.php @@ -100,19 +100,32 @@ public function dropIfExists($table): void /** * Get all the tables for the database; excluding ArangoDB system collections * + * @param string $name + * @return array + * + * @throws ArangoException + */ + public function getTable($name): array + { + return (array) $this->schemaManager->getCollectionStatistics($name); + } + + /** + * Get all the tables for the database; including ArangoDB system tables + * * @return array * * @throws ArangoException */ public function getAllTables(): array { - return $this->schemaManager->getCollections(true); + return $this->schemaManager->getCollections(false); } /** * Get the tables that belong to the database. * - * @return array + * @return array * @throws ArangoException */ public function getTables() @@ -150,7 +163,7 @@ public function drop($table) */ public function dropAllTables(): void { - $collections = $this->getAllTables(); + $collections = $this->getTables(true); foreach ($collections as $name) { $this->schemaManager->deleteCollection($name->name); diff --git a/src/Schema/Concerns/HandlesAnalyzers.php b/src/Schema/Concerns/HandlesAnalyzers.php index f086991..466a168 100644 --- a/src/Schema/Concerns/HandlesAnalyzers.php +++ b/src/Schema/Concerns/HandlesAnalyzers.php @@ -62,7 +62,7 @@ public function hasAnalyzer(string $analyzer): bool /** * @throws ArangoException */ - public function getAllAnalyzers(): array + public function getAnalyzers(): array { return $this->schemaManager->getAnalyzers(); } diff --git a/src/Schema/Concerns/HandlesGraphs.php b/src/Schema/Concerns/HandlesGraphs.php index 190d600..bb03245 100644 --- a/src/Schema/Concerns/HandlesGraphs.php +++ b/src/Schema/Concerns/HandlesGraphs.php @@ -37,7 +37,7 @@ public function getGraph(string $name): \stdClass /** * @throws ArangoException */ - public function getAllGraphs(): array + public function getGraphs(): array { return $this->schemaManager->getGraphs(); } diff --git a/src/Schema/Concerns/HandlesViews.php b/src/Schema/Concerns/HandlesViews.php index 5b09842..f36cffd 100644 --- a/src/Schema/Concerns/HandlesViews.php +++ b/src/Schema/Concerns/HandlesViews.php @@ -40,7 +40,7 @@ public function hasView($view) /** * @throws ArangoException */ - public function getAllViews(): array + public function getViews(): array { return $this->schemaManager->getViews(); } diff --git a/tests/Console/DbShowCommandTest.php b/tests/Console/DbShowCommandTest.php new file mode 100644 index 0000000..d06cd57 --- /dev/null +++ b/tests/Console/DbShowCommandTest.php @@ -0,0 +1,95 @@ +artisan('db:show') + ->expectsOutputToContain('Connection') + ->expectsOutputToContain('Database') + ->expectsOutputToContain('Analyzers') + ->expectsOutputToContain('Views') + ->expectsOutputToContain('Named Graphs') + ->expectsOutputToContain('characters') + ->expectsOutputToContain('users') + ->assertSuccessful(); +}); + +test('db:show --counts', function () { + $this->artisan( + 'db:show', + [ + '--counts' => true, + ], + ) + ->expectsOutputToContain('Size Estimate / Rows') + ->expectsOutputToContain('/ 43') + ->assertSuccessful(); +}); + + +test('db:show --analyzers', function () { + $this->artisan( + 'db:show', + [ + '--analyzers' => true, + ], + ) + ->expectsOutputToContain('Analyzers') + ->expectsOutputToContain('text_nl') + ->expectsOutputToContain('identity') + ->assertSuccessful(); +}); + +test('db:show --views', function () { + $this->artisan( + 'db:show', + [ + '--views' => true, + ], + ) + ->expectsOutputToContain('View') + ->expectsOutputToContain('house_search_alias_view') + ->expectsOutputToContain('arangosearch') + ->assertSuccessful(); +}); + +test('db:show --graphs', function () { + $this->schemaManager = $this->connection->getArangoClient()->schema(); + + $this->schemaManager->createGraph( + 'relatives', + [ + 'edgeDefinitions' => [ + [ + 'collection' => 'children', + 'from' => ['characters'], + 'to' => ['characters'], + ], + ], + ], + ); + + $this->artisan( + 'db:show', + [ + '--graphs' => true, + ], + ) + ->expectsOutputToContain('Graphs') + ->expectsOutputToContain('relatives') + ->assertSuccessful(); + + + $this->schemaManager->deleteGraph('relatives'); +}); + +test('db:show --system', function () { + $this->artisan( + 'db:show', + [ + '--system' => true, + ], + ) + ->expectsOutputToContain('_analyzers') + ->expectsOutputToContain('_jobs') + ->expectsOutputToContain('_queues') + ->assertSuccessful(); +}); diff --git a/tests/Pest.php b/tests/Pest.php index f76a4c4..7c223d6 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -78,23 +78,6 @@ function refreshDatabase() ]); } -/** - * The parameters that should be used when running "migrate:fresh". - * - * @return array - */ -//function migrateFreshUsing() -//{ -// ray('my migrateFreshUsing Pest.php'); -// return [ -// '--realpath' => true, -// '--path' => __DIR__ . '/../vendor/orchestra/testbench-core/laravel/migrations/', -// '--seed' => true, -// '--seeder' => DatabaseSeeder::class, -// ]; -//} - - function runCommand($command, $input = []) { return $command->run(new ArrayInput($input), new NullOutput()); diff --git a/tests/Schema/AnalyzerTest.php b/tests/Schema/AnalyzerTest.php index bdcf8f1..b662f5b 100644 --- a/tests/Schema/AnalyzerTest.php +++ b/tests/Schema/AnalyzerTest.php @@ -19,10 +19,10 @@ $schemaManager->deleteAnalyzer('myAnalyzer'); }); -test('getAllAnalyzers', function () { +test('getAnalyzers', function () { $schemaManager = $this->connection->getArangoClient()->schema(); - $analyzers = Schema::getAllAnalyzers(); + $analyzers = Schema::getAnalyzers(); expect($analyzers)->toHaveCount(13); }); @@ -67,16 +67,16 @@ test('dropAllAnalyzers', function () { $schemaManager = $this->connection->getArangoClient()->schema(); - $initialAnalyzers = Schema::getAllAnalyzers(); + $initialAnalyzers = Schema::getAnalyzers(); Schema::createAnalyzer('myAnalyzer1', 'identity'); Schema::createAnalyzer('myAnalyzer2', 'identity'); - $totalAnalyzers = Schema::getAllAnalyzers(); + $totalAnalyzers = Schema::getAnalyzers(); Schema::dropAllAnalyzers(); - $endAnalyzers = Schema::getAllAnalyzers(); + $endAnalyzers = Schema::getAnalyzers(); expect(count($initialAnalyzers))->toBe(count($endAnalyzers)); expect(count($initialAnalyzers))->toBe(count($totalAnalyzers) - 2); diff --git a/tests/Schema/GraphTest.php b/tests/Schema/GraphTest.php index 0de0a2c..9358f00 100644 --- a/tests/Schema/GraphTest.php +++ b/tests/Schema/GraphTest.php @@ -38,17 +38,17 @@ function createGraph() expect($graphExists)->toBeFalse(); }); -test('getAllGraphs', function () { +test('getGraphs', function () { $schemaManager = $this->connection->getArangoClient()->schema(); - $graphs = Schema::getAllGraphs(); + $graphs = Schema::getGraphs(); expect($graphs)->toHaveCount(0); if (!$schemaManager->hasGraph('myGraph')) { createGraph(); } - $graphs = Schema::getAllGraphs(); + $graphs = Schema::getGraphs(); expect($graphs)->toHaveCount(1); }); @@ -82,16 +82,16 @@ function createGraph() test('dropAllGraphs', function () { $schemaManager = $this->connection->getArangoClient()->schema(); - $initialGraphs = Schema::getAllGraphs(); + $initialGraphs = Schema::getGraphs(); Schema::createGraph('myGraph1'); Schema::createGraph('myGraph2'); - $totalGraphs = Schema::getAllGraphs(); + $totalGraphs = Schema::getGraphs(); Schema::dropAllGraphs(); - $endGraphs = Schema::getAllGraphs(); + $endGraphs = Schema::getGraphs(); expect(count($initialGraphs))->toBe(count($endGraphs)); expect(count($initialGraphs))->toBe(count($totalGraphs) - 2); diff --git a/tests/Schema/SchemaBuilderTest.php b/tests/Schema/SchemaBuilderTest.php index dbf5f55..a0f40ef 100644 --- a/tests/Schema/SchemaBuilderTest.php +++ b/tests/Schema/SchemaBuilderTest.php @@ -45,10 +45,10 @@ }); test('drop all tables', function () { - $initialTables = Schema::getAllTables(); + $initialTables = Schema::getTables(); Schema::dropAllTables(); - $tables = Schema::getAllTables(); + $tables = Schema::getTables(); expect(count($initialTables))->toEqual($this->tableCount); expect(count($tables))->toEqual(0); @@ -102,7 +102,7 @@ Schema::createView('search', []); } - $views = Schema::getAllViews(); + $views = Schema::getViews(); expect($views)->toHaveCount(5); expect($views[0]->name)->toBe('house_search_alias_view'); @@ -226,10 +226,10 @@ $schemaManager->deleteAnalyzer('myAnalyzer'); }); -test('getAllAnalyzers', function () { +test('getAnalyzers', function () { $schemaManager = $this->connection->getArangoClient()->schema(); - $analyzers = Schema::getAllAnalyzers(); + $analyzers = Schema::getAnalyzers(); expect($analyzers)->toHaveCount(13); }); diff --git a/tests/Schema/TableTest.php b/tests/Schema/TableTest.php index 516d297..05af1ea 100644 --- a/tests/Schema/TableTest.php +++ b/tests/Schema/TableTest.php @@ -95,11 +95,11 @@ }); test('dropAllTables', function () { - $initialTables = Schema::getAllTables(); + $initialTables = Schema::getTables(); Schema::dropAllTables(); - $tables = Schema::getAllTables(); + $tables = Schema::getTables(); expect(count($initialTables))->toEqual($this->tableCount); expect(count($tables))->toEqual(0); diff --git a/tests/Schema/ViewTest.php b/tests/Schema/ViewTest.php index 806fe80..03dc464 100644 --- a/tests/Schema/ViewTest.php +++ b/tests/Schema/ViewTest.php @@ -28,7 +28,7 @@ $schemaManager->deleteView('search'); }); -test('getAllViews', function () { +test('getViews', function () { $schemaManager = $this->connection->getArangoClient()->schema(); if (!$schemaManager->hasView('pages')) { Schema::createView('pages', []); @@ -40,7 +40,7 @@ Schema::createView('search', []); } - $views = Schema::getAllViews(); + $views = Schema::getViews(); expect($views)->toHaveCount(5); expect($views[0]->name)->toBe('house_search_alias_view'); diff --git a/tests/Testing/DatabaseTruncationTest.php b/tests/Testing/DatabaseTruncationTest.php index b508707..de1cf2d 100644 --- a/tests/Testing/DatabaseTruncationTest.php +++ b/tests/Testing/DatabaseTruncationTest.php @@ -9,7 +9,7 @@ uses(DatabaseTruncation::class); test('Ensure all tables are present', function () { - $tables = Schema::getAllTables(); + $tables = Schema::getTables(); expect(count($tables))->toEqual($this->tableCount); }); From 9991ed1df1586356c012239866760abe447c7941 Mon Sep 17 00:00:00 2001 From: Laravel Freelancer NL <36150929+LaravelFreelancerNL@users.noreply.github.com> Date: Thu, 5 Dec 2024 22:00:41 +0100 Subject: [PATCH 13/34] 135 support dbtable (#179) * tests: added computed value to test migration * feat: added getIndex, getIndexes and columns to the Schema Builder * tests: fixed test for added computed value * feat: Fixed and extended db:table to show ArangoDB related data --- ...9_11_15_000000_create_characters_table.php | 17 +- docs/console-commands.md | 5 + src/Console/TableCommand.php | 214 ++++++++++++++++++ src/Providers/CommandServiceProvider.php | 2 + src/Schema/Builder.php | 57 +++-- src/Schema/Concerns/HandlesIndexes.php | 54 +++++ src/Schema/Grammar.php | 37 +++ tests/Console/DbTableCommandTest.php | 61 +++++ tests/Query/SelectTest.php | 2 +- 9 files changed, 424 insertions(+), 25 deletions(-) create mode 100644 src/Console/TableCommand.php create mode 100644 src/Schema/Concerns/HandlesIndexes.php create mode 100644 tests/Console/DbTableCommandTest.php diff --git a/TestSetup/Database/Migrations/2019_11_15_000000_create_characters_table.php b/TestSetup/Database/Migrations/2019_11_15_000000_create_characters_table.php index 64dd563..904c178 100644 --- a/TestSetup/Database/Migrations/2019_11_15_000000_create_characters_table.php +++ b/TestSetup/Database/Migrations/2019_11_15_000000_create_characters_table.php @@ -14,7 +14,22 @@ */ public function up() { - Schema::create('characters', function (Blueprint $collection) {}); + Schema::create( + 'characters', + function (Blueprint $collection) {}, + [ + 'computedValues' => [ + [ + 'name' => 'full_name', + 'expression' => "RETURN CONCAT_SEPARATOR(' ', @doc.name, @doc.surname)", + 'overwrite' => true, + 'computeOn' => ["insert"], + 'failOnWarning' => false, + 'keepNull' => true, + ], + ], + ], + ); } /** diff --git a/docs/console-commands.md b/docs/console-commands.md index 637f365..0d5c781 100644 --- a/docs/console-commands.md +++ b/docs/console-commands.md @@ -18,6 +18,11 @@ In addition to the default Laravel options, you have the following: * --graphs: show a list of available named graphs * --system: include system tables in the table list +## db:table +db:table gives you an overview of the selected table. With ArangoDB specific information. + +The new --system option allows you to select a system table as well. + ## Migrations _**Migrations for ArangoDB use a different Schema blueprint. Therefore, you either need to run the convert:migrations command first, or convert them manually**_ diff --git a/src/Console/TableCommand.php b/src/Console/TableCommand.php new file mode 100644 index 0000000..67798e7 --- /dev/null +++ b/src/Console/TableCommand.php @@ -0,0 +1,214 @@ +connection($this->input->getOption('database')); + + if (! $connection instanceof Connection) { + return parent::handle($connections); + } + + $schema = $connection->getSchemaBuilder(); + + $tables = collect( + ($this->input->getOption('system')) + ? $schema->getAllTables() + : $schema->getTables(), + ) + ->keyBy(fn($table) => (string) $table->name) + ->all(); + + $tableName = (string) $this->argument('table') ?: select( + 'Which table would you like to inspect?', + array_keys($tables), + ); + + $table = $schema->getTable((string) $tableName); + + if (! $table) { + $this->components->warn("Table [{$tableName}] doesn't exist."); + + return 1; + } + + $tableName = $this->withoutTablePrefix($connection, $table['name']); + + $columns = $this->columns($schema, $tableName); + $indexes = $this->indexes($schema, $tableName); + + $data = [ + 'table' => $table, + 'columns' => $columns, + 'indexes' => $indexes, + ]; + + $this->display($data); + + return 0; + } + + /** + * Get the information regarding the table's columns. + * + * @param \Illuminate\Database\Schema\Builder $schema + * @param string $table + * @return \Illuminate\Support\Collection + */ + protected function columns(Builder $schema, string $table) + { + return collect($schema->getColumns($table)); + } + + /** + * Get the information regarding the table's indexes. + * + * @param \Illuminate\Database\Schema\Builder $schema + * @param string $table + * @return \Illuminate\Support\Collection + */ + protected function indexes(Builder $schema, string $table) + { + return collect($schema->getIndexes($table))->map(fn($index) => [ + 'name' => (string) $index['name'], + 'columns' => collect((array) $index['fields']), + 'attributes' => $this->getAttributesForIndex((array) $index), + ]); + } + + /** + * Get the attributes for a table index. + * + * @param array $index + * @return \Illuminate\Support\Collection + */ + protected function getAttributesForIndex($index) + { + return collect( + array_filter([ + 'sparse' => $index['sparse'] ? 'sparse' : null, + 'unique' => $index['unique'] ? 'unique' : null, + 'type' => $index['type'], + ]), + )->filter(); + } + + /** + * Render the table information. + * + * @param mixed[] $data + * @return void + */ + protected function display(array $data) + { + $this->option('json') ? $this->displayJson($data) : $this->displayForCli($data); + } + + protected function displayLongStringValue(string $value): string + { + if (strlen($value) < 136) { + return $value; + } + return substr($value, 0, 133) . '...'; + } + + /** + * Render the table information formatted for the CLI. + * + * @param mixed[] $data + * @return void + */ + protected function displayForCli(array $data) + { + [$table, $columns, $indexes ] = [ + $data['table'], $data['columns'], $data['indexes'], + ]; + + $this->newLine(); + + $this->components->twoColumnDetail('Table', '' . $table['name'] . ''); + $this->components->twoColumnDetail('Type', ($table['type'] == 2) ? 'Vertex' : 'Edge'); + $this->components->twoColumnDetail('Status', $table['statusString']); + $this->components->twoColumnDetail('User Keys Allowed', ($table['keyOptions']->allowUserKeys) ? 'Yes' : 'No'); + $this->components->twoColumnDetail('Key Type', $table['keyOptions']->type); + $this->components->twoColumnDetail('Last Used Key', $table['keyOptions']->lastValue); + $this->components->twoColumnDetail('Wait For Sync', ($table['waitForSync']) ? 'Yes' : 'No'); + $this->components->twoColumnDetail('Columns', $table['count']); + $this->components->twoColumnDetail('Size Estimate', Number::fileSize($table['figures']->documentsSize, 2)); + + $this->newLine(); + + if ($columns->isNotEmpty()) { + $this->components->twoColumnDetail('Column', 'Type'); + + $columns->each(function ($column) { + $this->components->twoColumnDetail( + $column['name'], + implode(', ', $column['types']), + ); + }); + $this->components->info('ArangoDB is schemaless by default. Hence, the column & types are a representation of current data within the table.'); + } + + $computedValues = collect((array) $table['computedValues']); + if ($computedValues->isNotEmpty()) { + $this->components->twoColumnDetail('Computed Value', 'Expression'); + + $computedValues->each(function ($value) { + $this->components->twoColumnDetail( + $value->name, + $this->displayLongStringValue($value->expression), + ); + }); + + $this->newLine(); + } + + if ($indexes->isNotEmpty()) { + $this->components->twoColumnDetail('Index'); + + $indexes->each(function ($index) { + $this->components->twoColumnDetail( + $index['name'] . ' ' . $index['columns']->implode(', ') . '', + $index['attributes']->implode(', '), + ); + }); + + $this->newLine(); + } + } +} diff --git a/src/Providers/CommandServiceProvider.php b/src/Providers/CommandServiceProvider.php index 2dd1c2b..54a4ee4 100644 --- a/src/Providers/CommandServiceProvider.php +++ b/src/Providers/CommandServiceProvider.php @@ -5,6 +5,7 @@ namespace LaravelFreelancerNL\Aranguent\Providers; use LaravelFreelancerNL\Aranguent\Console\ShowCommand; +use LaravelFreelancerNL\Aranguent\Console\TableCommand; use LaravelFreelancerNL\Aranguent\Console\WipeCommand; use LaravelFreelancerNL\Aranguent\Console\DbCommand; use Illuminate\Database\Console\DbCommand as IlluminateDbCommand; @@ -25,6 +26,7 @@ class CommandServiceProvider extends ServiceProvider 'Db' => DbCommand::class, 'DbWipe' => WipeCommand::class, 'DbShow' => ShowCommand::class, + 'DbTable' => TableCommand::class, ]; diff --git a/src/Schema/Builder.php b/src/Schema/Builder.php index 25a5d45..6cd1d74 100644 --- a/src/Schema/Builder.php +++ b/src/Schema/Builder.php @@ -12,6 +12,7 @@ use LaravelFreelancerNL\Aranguent\Connection; use LaravelFreelancerNL\Aranguent\Exceptions\QueryException; use LaravelFreelancerNL\Aranguent\Schema\Concerns\HandlesAnalyzers; +use LaravelFreelancerNL\Aranguent\Schema\Concerns\HandlesIndexes; use LaravelFreelancerNL\Aranguent\Schema\Concerns\HandlesIndexNaming; use LaravelFreelancerNL\Aranguent\Schema\Concerns\HandlesGraphs; use LaravelFreelancerNL\Aranguent\Schema\Concerns\HandlesViews; @@ -22,6 +23,7 @@ class Builder extends \Illuminate\Database\Schema\Builder use HandlesAnalyzers; use HandlesIndexNaming; use HandlesGraphs; + use HandlesIndexes; use HandlesViews; use UsesBlueprints; @@ -187,46 +189,46 @@ public function hasColumn($table, $column) * * @param string $table * @param string|string[] $columns - * @return bool + * @return array */ - public function hasColumns($table, $columns) + public function getColumns($table) { - if (is_string($columns)) { - $columns = [$columns]; - } - $parameters = []; - $parameters['name'] = 'hasColumn'; + $parameters['name'] = 'columns'; $parameters['handler'] = 'aql'; - $parameters['columns'] = $columns; + $parameters['table'] = $table; $command = new Fluent($parameters); - $compilation = $this->grammar->compileHasColumn($table, $command); - return $this->connection->select($compilation['aqb'])[0]; + $compilation = $this->grammar->compileColumns($table, $command); + + $rawColumns = $this->connection->select($compilation['aqb'], $compilation['bindings']); + + return $this->mapResultsToArray($rawColumns); } /** - * Determine if the given table has a given index. + * Determine if the given table has given columns. * - * @param string $table - * @param string|array $index - * @param string|null $type + * @param string $table + * @param string|string[] $columns * @return bool */ - public function hasIndex($table, $index, $type = null, array $options = []) + public function hasColumns($table, $columns) { - $name = $index; - - if ($type === null) { - $type = 'persistent'; + if (is_string($columns)) { + $columns = [$columns]; } - if (is_array($index)) { - $name = $this->createIndexName($type, $index, $options, $table); - } + $parameters = []; + $parameters['name'] = 'hasColumn'; + $parameters['handler'] = 'aql'; + $parameters['columns'] = $columns; - return !!$this->schemaManager->getIndexByName($table, $name); + $command = new Fluent($parameters); + + $compilation = $this->grammar->compileHasColumn($table, $command); + return $this->connection->select($compilation['aqb'])[0]; } /** @@ -295,6 +297,15 @@ public function withoutForeignKeyConstraints(Closure $callback) return $callback(); } + /** + * @param mixed[] $results + * @return mixed[] + */ + protected function mapResultsToArray($results) + { + return array_map(function ($result) { return (array) $result; }, $results); + } + /** * Silently catch the use of unsupported builder methods. */ diff --git a/src/Schema/Concerns/HandlesIndexes.php b/src/Schema/Concerns/HandlesIndexes.php new file mode 100644 index 0000000..fb44315 --- /dev/null +++ b/src/Schema/Concerns/HandlesIndexes.php @@ -0,0 +1,54 @@ + $index + * @param string|null $type + * @return bool + */ + public function hasIndex($table, $index, $type = null, array $options = []) + { + $name = $index; + + if ($type === null) { + $type = 'persistent'; + } + + if (is_array($index)) { + $name = $this->createIndexName($type, $index, $options, $table); + } + + return !!$this->schemaManager->getIndexByName($table, $name); + } + + /** + * @param string $id + * @return array + */ + public function getIndex(string $id) + { + return (array) $this->schemaManager->getIndex($id); + } + + /** + * @param string $table + * @return mixed[] + * @throws ArangoException + */ + public function getIndexes($table) + { + return $this->mapResultsToArray( + $this->schemaManager->getIndexes($table), + ); + } +} diff --git a/src/Schema/Grammar.php b/src/Schema/Grammar.php index 9f31fb1..5f68455 100644 --- a/src/Schema/Grammar.php +++ b/src/Schema/Grammar.php @@ -19,6 +19,43 @@ class Grammar extends IlluminateGrammar */ protected $transactions = false; + /** + * Compile AQL to check if an attribute is in use within a document in the collection. + * If multiple attributes are set then all must be set in one document. + * + * @param string $table + * @return Fluent + * @throws BindException + */ + public function compileColumns($table, Fluent $command) + { + $command->bindings = [ + '@collection' => $table, + ]; + + $command->aqb = sprintf( + 'LET rawColumns = MERGE_RECURSIVE( + ( + FOR doc IN @@collection + LET fields = ATTRIBUTES(doc, true, true) + FOR field IN fields + RETURN { + [field]: { + [TYPENAME(doc[field])]: true + } + } + ) +) +FOR column IN ATTRIBUTES(rawColumns) + RETURN { + name: column, + types: ATTRIBUTES(rawColumns[column]) + }', + $table, + ); + + return $command; + } /** * Compile AQL to check if an attribute is in use within a document in the collection. * If multiple attributes are set then all must be set in one document. diff --git a/tests/Console/DbTableCommandTest.php b/tests/Console/DbTableCommandTest.php new file mode 100644 index 0000000..79617c3 --- /dev/null +++ b/tests/Console/DbTableCommandTest.php @@ -0,0 +1,61 @@ +artisan('db:table') + ->expectsQuestion('Which table would you like to inspect?', 'children') + ->expectsOutputToContain('children') + ->expectsOutputToContain('Edge') + ->expectsOutputToContain('User Keys Allowed') + ->expectsOutputToContain('Key Type') + ->expectsOutputToContain('Last Used Key') + ->expectsOutputToContain('Wait For Sync') + ->expectsOutputToContain('Columns') + ->expectsOutputToContain('Size Estimate') + ->expectsOutputToContain('primary _key') + ->expectsOutputToContain('edge') + ->assertSuccessful(); +}); + +test('db:table children', function () { + $this->artisan('db:table', ['table' => 'children']) + ->expectsOutputToContain('children') + ->expectsOutputToContain('Edge') + ->expectsOutputToContain('User Keys Allowed') + ->expectsOutputToContain('Key Type') + ->expectsOutputToContain('Last Used Key') + ->expectsOutputToContain('Wait For Sync') + ->expectsOutputToContain('Columns') + ->expectsOutputToContain('Size Estimate') + ->expectsOutputToContain('primary _key') + ->expectsOutputToContain('edge') + ->assertSuccessful(); +}); + +test('db:table characters', function () { + $this->artisan('db:table', ['table' => 'characters']) + ->expectsOutputToContain('characters') + ->expectsOutputToContain('Vertex') + ->expectsOutputToContain('User Keys Allowed') + ->expectsOutputToContain('Key Type') + ->expectsOutputToContain('Last Used Key') + ->expectsOutputToContain('Wait For Sync') + ->expectsOutputToContain('Columns') + ->expectsOutputToContain('Size Estimate') + ->expectsOutputToContain('primary _key') + ->expectsOutputToContain('full_name') + ->assertSuccessful(); +}); + +test('db:table _job', function () { + $this->artisan('db:table', ['table' => '_jobs']) + ->expectsOutputToContain('_jobs') + ->expectsOutputToContain('Vertex') + ->expectsOutputToContain('User Keys Allowed') + ->expectsOutputToContain('Key Type') + ->expectsOutputToContain('Last Used Key') + ->expectsOutputToContain('Wait For Sync') + ->expectsOutputToContain('Columns') + ->expectsOutputToContain('Size Estimate') + ->expectsOutputToContain('primary _key') + ->assertSuccessful(); +}); diff --git a/tests/Query/SelectTest.php b/tests/Query/SelectTest.php index 89d51af..5943e2e 100644 --- a/tests/Query/SelectTest.php +++ b/tests/Query/SelectTest.php @@ -7,7 +7,7 @@ expect($results)->toHaveCount(43); - expect(count((array) $results[0]))->toBe(9); + expect(count((array) $results[0]))->toBe(10); }); test('basic select with specific column', function () { From fae1a8f33897f7f642af96d75228a5f1111ba422 Mon Sep 17 00:00:00 2001 From: Bas Date: Sun, 15 Dec 2024 19:31:27 +0100 Subject: [PATCH 14/34] fix: Schema structure methods like getTable or getTables now all return arrays or lists of arrays to conform to the Laravel standard. --- src/Console/ShowCommand.php | 14 +++++++------- src/Console/TableCommand.php | 4 ++-- src/Schema/Builder.php | 10 +++++++--- src/Schema/Concerns/HandlesAnalyzers.php | 10 +++++++--- src/Schema/Concerns/HandlesGraphs.php | 11 ++++++++--- src/Schema/Concerns/HandlesViews.php | 11 ++++++++--- tests/Schema/SchemaBuilderTest.php | 12 ++++++------ tests/Schema/ViewTest.php | 12 ++++++------ 8 files changed, 51 insertions(+), 33 deletions(-) diff --git a/src/Console/ShowCommand.php b/src/Console/ShowCommand.php index b5036cd..5da75d1 100644 --- a/src/Console/ShowCommand.php +++ b/src/Console/ShowCommand.php @@ -119,7 +119,7 @@ protected function tables(ConnectionInterface $connection, $schema) // Get per table statistics $tableStats = []; foreach ($tables as $table) { - $tableStats[] = $schema->getTable($table->name); + $tableStats[] = $schema->getTable($table['name']); } return collect($tableStats)->map(fn($table) => [ @@ -148,8 +148,8 @@ protected function views(ConnectionInterface $connection, IlluminateSchemaBuilde return collect($schema->getViews()) ->map(fn($view) => [ - 'name' => $view->name, - 'type' => $view->type, + 'name' => $view['name'], + 'type' => $view['type'], ]); } @@ -163,8 +163,8 @@ protected function analyzers(SchemaBuilder $schema) { return collect($schema->getAnalyzers()) ->map(fn($analyzer) => [ - 'name' => $analyzer->name, - 'type' => $analyzer->type, + 'name' => $analyzer['name'], + 'type' => $analyzer['type'], ]); } @@ -178,8 +178,8 @@ protected function graphs(SchemaBuilder $schema) { return collect($schema->getGraphs()) ->map(fn($graph) => [ - 'name' => $graph->name, - 'edgeDefinitions' => count($graph->edgeDefinitions), + 'name' => $graph['name'], + 'edgeDefinitions' => count($graph['edgeDefinitions']), ]); } diff --git a/src/Console/TableCommand.php b/src/Console/TableCommand.php index 67798e7..5302e93 100644 --- a/src/Console/TableCommand.php +++ b/src/Console/TableCommand.php @@ -50,7 +50,7 @@ public function handle(ConnectionResolverInterface $connections) ? $schema->getAllTables() : $schema->getTables(), ) - ->keyBy(fn($table) => (string) $table->name) + ->keyBy(fn($table) => (string) $table['name']) ->all(); $tableName = (string) $this->argument('table') ?: select( @@ -178,7 +178,7 @@ protected function displayForCli(array $data) $columns->each(function ($column) { $this->components->twoColumnDetail( $column['name'], - implode(', ', $column['types']), + implode(', ', $column['type']), ); }); $this->components->info('ArangoDB is schemaless by default. Hence, the column & types are a representation of current data within the table.'); diff --git a/src/Schema/Builder.php b/src/Schema/Builder.php index 6cd1d74..f8bd902 100644 --- a/src/Schema/Builder.php +++ b/src/Schema/Builder.php @@ -121,7 +121,9 @@ public function getTable($name): array */ public function getAllTables(): array { - return $this->schemaManager->getCollections(false); + return $this->mapResultsToArray( + $this->schemaManager->getCollections(false), + ); } /** @@ -132,7 +134,9 @@ public function getAllTables(): array */ public function getTables() { - return $this->schemaManager->getCollections(true); + return $this->mapResultsToArray( + $this->schemaManager->getCollections(true), + ); } /** @@ -168,7 +172,7 @@ public function dropAllTables(): void $collections = $this->getTables(true); foreach ($collections as $name) { - $this->schemaManager->deleteCollection($name->name); + $this->schemaManager->deleteCollection($name['name']); } } diff --git a/src/Schema/Concerns/HandlesAnalyzers.php b/src/Schema/Concerns/HandlesAnalyzers.php index 466a168..3da7393 100644 --- a/src/Schema/Concerns/HandlesAnalyzers.php +++ b/src/Schema/Concerns/HandlesAnalyzers.php @@ -45,11 +45,13 @@ public function replaceAnalyzer(string $name, string $type, array $properties = } /** + * @param string $name + * @return mixed[] * @throws ArangoException */ - public function getAnalyzer(string $name): \stdClass + public function getAnalyzer(string $name): array { - return $this->schemaManager->getAnalyzer($name); + return (array) $this->schemaManager->getAnalyzer($name); } public function hasAnalyzer(string $analyzer): bool @@ -64,7 +66,9 @@ public function hasAnalyzer(string $analyzer): bool */ public function getAnalyzers(): array { - return $this->schemaManager->getAnalyzers(); + return $this->mapResultsToArray( + $this->schemaManager->getAnalyzers(), + ); } /** diff --git a/src/Schema/Concerns/HandlesGraphs.php b/src/Schema/Concerns/HandlesGraphs.php index bb03245..db3ea48 100644 --- a/src/Schema/Concerns/HandlesGraphs.php +++ b/src/Schema/Concerns/HandlesGraphs.php @@ -27,19 +27,24 @@ public function hasGraph(string $name): bool } /** + * @param string $name + * @return mixed[] * @throws ArangoException */ - public function getGraph(string $name): \stdClass + public function getGraph(string $name): array { - return $this->schemaManager->getGraph($name); + return (array) $this->schemaManager->getGraph($name); } /** + * @return mixed[] * @throws ArangoException */ public function getGraphs(): array { - return $this->schemaManager->getGraphs(); + return $this->mapResultsToArray( + $this->schemaManager->getGraphs(), + ); } /** diff --git a/src/Schema/Concerns/HandlesViews.php b/src/Schema/Concerns/HandlesViews.php index f36cffd..a9a744f 100644 --- a/src/Schema/Concerns/HandlesViews.php +++ b/src/Schema/Concerns/HandlesViews.php @@ -23,11 +23,13 @@ public function createView(string $name, array $properties, string $type = 'aran } /** + * @param string $name + * @return mixed[] * @throws ArangoException */ - public function getView(string $name): \stdClass + public function getView(string $name): array { - return $this->schemaManager->getView($name); + return (array) $this->schemaManager->getView($name); } public function hasView($view) @@ -38,11 +40,14 @@ public function hasView($view) } /** + * @return mixed[] * @throws ArangoException */ public function getViews(): array { - return $this->schemaManager->getViews(); + return $this->mapResultsToArray( + $this->schemaManager->getViews(), + ); } /** diff --git a/tests/Schema/SchemaBuilderTest.php b/tests/Schema/SchemaBuilderTest.php index a0f40ef..858c12b 100644 --- a/tests/Schema/SchemaBuilderTest.php +++ b/tests/Schema/SchemaBuilderTest.php @@ -85,7 +85,7 @@ } $view = Schema::getView('search'); - expect($view->name)->toEqual('search'); + expect($view['name'])->toEqual('search'); $schemaManager->deleteView('search'); }); @@ -105,11 +105,11 @@ $views = Schema::getViews(); expect($views)->toHaveCount(5); - expect($views[0]->name)->toBe('house_search_alias_view'); - expect($views[1]->name)->toBe('house_view'); - expect($views[2]->name)->toBe('pages'); - expect($views[3]->name)->toBe('products'); - expect($views[4]->name)->toBe('search'); + expect($views[0]['name'])->toBe('house_search_alias_view'); + expect($views[1]['name'])->toBe('house_view'); + expect($views[2]['name'])->toBe('pages'); + expect($views[3]['name'])->toBe('products'); + expect($views[4]['name'])->toBe('search'); $schemaManager->deleteView('search'); $schemaManager->deleteView('pages'); diff --git a/tests/Schema/ViewTest.php b/tests/Schema/ViewTest.php index 03dc464..8cdce64 100644 --- a/tests/Schema/ViewTest.php +++ b/tests/Schema/ViewTest.php @@ -23,7 +23,7 @@ } $view = Schema::getView('search'); - expect($view->name)->toEqual('search'); + expect($view['name'])->toEqual('search'); $schemaManager->deleteView('search'); }); @@ -43,11 +43,11 @@ $views = Schema::getViews(); expect($views)->toHaveCount(5); - expect($views[0]->name)->toBe('house_search_alias_view'); - expect($views[1]->name)->toBe('house_view'); - expect($views[2]->name)->toBe('pages'); - expect($views[3]->name)->toBe('products'); - expect($views[4]->name)->toBe('search'); + expect($views[0]['name'])->toBe('house_search_alias_view'); + expect($views[1]['name'])->toBe('house_view'); + expect($views[2]['name'])->toBe('pages'); + expect($views[3]['name'])->toBe('products'); + expect($views[4]['name'])->toBe('search'); $schemaManager->deleteView('search'); $schemaManager->deleteView('pages'); From b9e74be119d1d033a0729134e94c6c2c0349be11 Mon Sep 17 00:00:00 2001 From: Bas Date: Sun, 15 Dec 2024 19:32:04 +0100 Subject: [PATCH 15/34] docs: Added getTable for better IDE support --- src/Facades/Schema.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Facades/Schema.php b/src/Facades/Schema.php index c014376..a78fe35 100644 --- a/src/Facades/Schema.php +++ b/src/Facades/Schema.php @@ -12,6 +12,7 @@ * Table handling: * * @method static Builder create($collection, Closure $callback, $options = []) + * @method static Builder getTable(string $table) * @method static Builder getTables() * @method static Builder drop(string $collection) * @method static Builder dropIfExists(string $collection) From 6cae124a05bc4aa76f323d9747ae6ca6db9b5a7d Mon Sep 17 00:00:00 2001 From: Bas Date: Sun, 15 Dec 2024 19:33:17 +0100 Subject: [PATCH 16/34] fix: changed 'types' column field to 'type' conform to Laravel sql output --- src/Schema/Grammar.php | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/Schema/Grammar.php b/src/Schema/Grammar.php index 5f68455..1f49b9c 100644 --- a/src/Schema/Grammar.php +++ b/src/Schema/Grammar.php @@ -6,6 +6,7 @@ use Illuminate\Database\Schema\Grammars\Grammar as IlluminateGrammar; use Illuminate\Support\Fluent; +use LaravelFreelancerNL\FluentAQL\Exceptions\BindException; use LaravelFreelancerNL\FluentAQL\QueryBuilder; class Grammar extends IlluminateGrammar @@ -35,22 +36,22 @@ public function compileColumns($table, Fluent $command) $command->aqb = sprintf( 'LET rawColumns = MERGE_RECURSIVE( - ( - FOR doc IN @@collection - LET fields = ATTRIBUTES(doc, true, true) - FOR field IN fields - RETURN { - [field]: { - [TYPENAME(doc[field])]: true - } - } - ) -) -FOR column IN ATTRIBUTES(rawColumns) - RETURN { - name: column, - types: ATTRIBUTES(rawColumns[column]) - }', + ( + FOR doc IN @@collection + LET fields = ATTRIBUTES(doc, true, true) + FOR field IN fields + RETURN { + [field]: { + [TYPENAME(doc[field])]: true + } + } + ) + ) + FOR column IN ATTRIBUTES(rawColumns) + RETURN { + name: column, + type: ATTRIBUTES(rawColumns[column]) + }', $table, ); From dc044389bd6825ff1954fc5e916ab560944168b4 Mon Sep 17 00:00:00 2001 From: Bas Date: Sun, 15 Dec 2024 19:33:56 +0100 Subject: [PATCH 17/34] fix: Fixed check on non-existant 'schema' array key. --- src/Testing/DatabaseTruncation.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Testing/DatabaseTruncation.php b/src/Testing/DatabaseTruncation.php index 28270dd..e1c41e2 100644 --- a/src/Testing/DatabaseTruncation.php +++ b/src/Testing/DatabaseTruncation.php @@ -37,4 +37,14 @@ protected function migrateFreshUsing() return $results; } + + /** + * Determine if a table exists in the given list, with or without its schema. + */ + protected function tableExistsIn(array $table, array $tables): bool + { + return isset($table['schema']) + ? ! empty(array_intersect([$table['name'], $table['schema'] . '.' . $table['name']], $tables)) + : in_array($table['name'], $tables); + } } From d02cd37989cf0afebdc55f0e8b1f3a6e0bfaf46f Mon Sep 17 00:00:00 2001 From: Bas Date: Sun, 15 Dec 2024 19:49:30 +0100 Subject: [PATCH 18/34] feat: support model:show artisan command --- src/AranguentServiceProvider.php | 6 + src/Console/ShowModelCommand.php | 175 +++++++++++++++++++++ src/Eloquent/ModelInspector.php | 187 +++++++++++++++++++++++ src/Providers/CommandServiceProvider.php | 14 +- tests/Console/ShowModelCommandTest.php | 26 ++++ 5 files changed, 402 insertions(+), 6 deletions(-) create mode 100644 src/Console/ShowModelCommand.php create mode 100644 src/Eloquent/ModelInspector.php create mode 100644 tests/Console/ShowModelCommandTest.php diff --git a/src/AranguentServiceProvider.php b/src/AranguentServiceProvider.php index d8f8578..14128d2 100644 --- a/src/AranguentServiceProvider.php +++ b/src/AranguentServiceProvider.php @@ -6,7 +6,9 @@ use Illuminate\Support\ServiceProvider; use LaravelFreelancerNL\Aranguent\Eloquent\Model; +use LaravelFreelancerNL\Aranguent\Eloquent\ModelInspector; use LaravelFreelancerNL\Aranguent\Schema\Grammar as SchemaGrammar; +use Illuminate\Database\Eloquent\ModelInspector as IlluminateModelInspector; class AranguentServiceProvider extends ServiceProvider { @@ -58,6 +60,10 @@ public function register() return $app['migrator']; }); + $this->app->extend(IlluminateModelInspector::class, function () { + return new ModelInspector($this->app); + }); + $this->app->resolving( 'db', function ($db) { diff --git a/src/Console/ShowModelCommand.php b/src/Console/ShowModelCommand.php new file mode 100644 index 0000000..24fe004 --- /dev/null +++ b/src/Console/ShowModelCommand.php @@ -0,0 +1,175 @@ +inspect( + $this->argument('model'), + $this->option('database'), + ); + } catch (BindingResolutionException $e) { + $this->components->error($e->getMessage()); + + return 1; + } + + $this->display( + $info['class'], + $info['database'], + $info['table'], + $info['policy'], + $info['attributes'], + $info['relations'], + $info['events'], + $info['observers'], + ); + + return 0; + } + + /** + * Render the model information. + * + * @param class-string<\Illuminate\Database\Eloquent\Model> $class + * @param string $database + * @param string $table + * @param class-string|null $policy + * @param Collection $attributes + * @param Collection $relations + * @param Collection $events + * @param Collection $observers + * @return void + */ + protected function display($class, $database, $table, $policy, $attributes, $relations, $events, $observers) + { + $this->option('json') + ? $this->displayJson($class, $database, $table, $policy, $attributes, $relations, $events, $observers) + : $this->displayCli($class, $database, $table, $policy, $attributes, $relations, $events, $observers); + } + + /** + * Render the model information for the CLI. + * + * @param class-string<\Illuminate\Database\Eloquent\Model> $class + * @param string $database + * @param string $table + * @param class-string|null $policy + * @param Collection $attributes + * @param Collection $relations + * @param Collection $events + * @param Collection $observers + * @return void + */ + protected function displayCli($class, $database, $table, $policy, $attributes, $relations, $events, $observers) + { + $this->newLine(); + + $this->components->twoColumnDetail('' . $class . ''); + $this->components->twoColumnDetail('Database', $database); + $this->components->twoColumnDetail('Table', $table); + + if ($policy) { + $this->components->twoColumnDetail('Policy', $policy); + } + + $this->newLine(); + + $this->components->twoColumnDetail( + 'Attributes', + 'type / cast', + ); + + foreach ($attributes as $attribute) { + $first = trim(sprintf( + '%s %s', + $attribute['name'], + collect(['computed', 'increments', 'unique', 'nullable', 'fillable', 'hidden', 'appended']) + ->filter(fn($property) => $attribute[$property]) + ->map(fn($property) => sprintf('%s', $property)) + ->implode(', '), + )); + + $second = collect([ + (is_array($attribute['type'])) ? implode(', ', $attribute['type']) : $attribute['type'], + $attribute['cast'] ? '' . $attribute['cast'] . '' : null, + ])->filter()->implode(' / '); + + $this->components->twoColumnDetail($first, $second); + } + + $this->newLine(); + + $this->components->twoColumnDetail('Relations'); + + foreach ($relations as $relation) { + $this->components->twoColumnDetail( + sprintf('%s %s', $relation['name'], $relation['type']), + $relation['related'], + ); + } + + $this->newLine(); + + $this->displayCliEvents($events, $observers); + + $this->newLine(); + } + + /** + * @param Collection $events + * @return void + */ + public function displayCliEvents(Collection $events, Collection $observers): void + { + $this->components->twoColumnDetail('Events'); + + if ($events->count()) { + foreach ($events as $event) { + $this->components->twoColumnDetail( + sprintf('%s', $event['event']), + sprintf('%s', $event['class']), + ); + } + } + + $this->newLine(); + + $this->components->twoColumnDetail('Observers'); + + if ($observers->count()) { + foreach ($observers as $observer) { + $this->components->twoColumnDetail( + sprintf('%s', $observer['event']), + implode(', ', $observer['observer']), + ); + } + } + + } + +} diff --git a/src/Eloquent/ModelInspector.php b/src/Eloquent/ModelInspector.php new file mode 100644 index 0000000..a0627b2 --- /dev/null +++ b/src/Eloquent/ModelInspector.php @@ -0,0 +1,187 @@ + + */ + protected $relationMethods = [ + 'hasMany', + 'hasManyThrough', + 'hasOneThrough', + 'belongsToMany', + 'hasOne', + 'belongsTo', + 'morphOne', + 'morphTo', + 'morphMany', + 'morphToMany', + 'morphedByMany', + ]; + + /** + * Extract model details for the given model. + * + * @param class-string<\Illuminate\Database\Eloquent\Model>|string $model + * @param string|null $connection + * @return array{"class": class-string<\Illuminate\Database\Eloquent\Model>, database: string, table: string, policy: class-string|null, attributes: Collection, relations: Collection, events: Collection, observers: Collection, collection: class-string<\Illuminate\Database\Eloquent\Collection<\Illuminate\Database\Eloquent\Model>>, builder: class-string<\Illuminate\Database\Eloquent\Builder<\Illuminate\Database\Eloquent\Model>>} + * + * @throws BindingResolutionException + */ + public function inspect($model, $connection = null) + { + $class = $this->qualifyModel($model); + + /** @var \Illuminate\Database\Eloquent\Model $model */ + $model = $this->app->make($class); + + if ($connection !== null) { + $model->setConnection($connection); + } + + + /* @phpstan-ignore-next-line */ + return [ + 'class' => get_class($model), + 'database' => $model->getConnection()->getName() ?? '', + 'table' => $model->getConnection()->getTablePrefix() . $model->getTable(), + 'policy' => $this->getPolicy($model) ?? '', + 'attributes' => $this->getAttributes($model), + 'relations' => $this->getRelations($model), + 'events' => $this->getEvents($model), + 'observers' => $this->getObservers($model), + 'collection' => $this->getCollectedBy($model), + 'builder' => $this->getBuilder($model), + ]; + } + + /** + * Get the column attributes for the given model. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @return Collection> + */ + protected function getAttributes($model) + { + $connection = $model->getConnection(); + assert($connection instanceof Connection); + + $schema = $connection->getSchemaBuilder(); + $table = $model->getTable(); + $tableData = $schema->getTable($table); + $columns = $schema->getColumns($table); + $indexes = $schema->getIndexes($table); + + $columns = $this->addSystemAttributes($columns, $tableData); + + /* @phpstan-ignore-next-line */ + return collect($columns) + ->map(fn($column) => [ + 'name' => $column['name'], + 'type' => $column['type'], + 'increments' => $column['auto_increment'] ?? null, + 'nullable' => $column['nullable'] ?? null, + 'default' => $this->getColumnDefault($column, $model) ?? null, + 'unique' => $this->columnIsUnique($column['name'], $indexes), + 'fillable' => $model->isFillable($column['name']), + 'computed' => $this->columnIsComputed($column['name'], $tableData), + 'hidden' => $this->attributeIsHidden($column['name'], $model), + 'appended' => null, + 'cast' => $this->getCastType($column['name'], $model), + ]) + ->merge($this->getVirtualAttributes($model, $columns)); + } + + /** + * Get the default value for the given column. + * + * @param array $column + * @param \Illuminate\Database\Eloquent\Model $model + * @return mixed|null + */ + protected function getColumnDefault($column, $model) + { + $attributeDefault = $model->getAttributes()[$column['name']] ?? null; + + return enum_value($attributeDefault, $column['default'] ?? null); + } + + /** + * Determine if the given attribute is unique. + * + * @param string $column + * @param mixed[] $indexes + * @return bool + */ + protected function columnIsUnique($column, $indexes) + { + return collect($indexes)->contains( + fn($index) => count($index['fields']) === 1 && $index['fields'][0] === $column && $index['unique'], + ); + } + + /** + * @param string $name + * @param array $tableData + * @return bool + */ + protected function columnIsComputed($name, $tableData) + { + $computedValues = (new Collection($tableData['computedValues']))->pluck('name')->toArray(); + + return in_array($name, $computedValues); + } + + /** + * @param mixed[] $columns + * @param mixed[] $tableData + * @return mixed[] + */ + protected function addSystemAttributes(array $columns, $tableData) + { + // edges add _from, _to + if ($tableData['type'] === 3) { + array_unshift( + $columns, + [ + 'name' => '_to', + 'type' => 'string', + 'nullable' => false, + ], + ); + array_unshift( + $columns, + [ + 'name' => '_from', + 'type' => 'string', + 'nullable' => false, + ], + ); + } + + // Prepend id, + array_unshift( + $columns, + [ + 'name' => 'id', + 'type' => $tableData['keyOptions']->type, + 'nullable' => false, + 'allowUserKeys' => $tableData['keyOptions']->allowUserKeys, + 'unique' => true, + ], + ); + + return $columns; + } +} diff --git a/src/Providers/CommandServiceProvider.php b/src/Providers/CommandServiceProvider.php index 54a4ee4..f2460bb 100644 --- a/src/Providers/CommandServiceProvider.php +++ b/src/Providers/CommandServiceProvider.php @@ -5,6 +5,7 @@ namespace LaravelFreelancerNL\Aranguent\Providers; use LaravelFreelancerNL\Aranguent\Console\ShowCommand; +use LaravelFreelancerNL\Aranguent\Console\ShowModelCommand; use LaravelFreelancerNL\Aranguent\Console\TableCommand; use LaravelFreelancerNL\Aranguent\Console\WipeCommand; use LaravelFreelancerNL\Aranguent\Console\DbCommand; @@ -27,6 +28,7 @@ class CommandServiceProvider extends ServiceProvider 'DbWipe' => WipeCommand::class, 'DbShow' => ShowCommand::class, 'DbTable' => TableCommand::class, + 'ShowModel' => ShowModelCommand::class, ]; @@ -63,17 +65,17 @@ protected function registerCommands(array $commands) $this->commands(array_values($commands)); } - protected function registerModelMakeCommand(): void + protected function registerDbCommand(): void { - $this->app->singleton(ModelMakeCommand::class, function ($app) { - return new ModelMakeCommand($app['files']); + $this->app->extend(IlluminateDbCommand::class, function () { + return new DbCommand(); }); } - protected function registerDbCommand(): void + protected function registerModelMakeCommand(): void { - $this->app->extend(IlluminateDbCommand::class, function () { - return new DbCommand(); + $this->app->singleton(ModelMakeCommand::class, function ($app) { + return new ModelMakeCommand($app['files']); }); } diff --git a/tests/Console/ShowModelCommandTest.php b/tests/Console/ShowModelCommandTest.php new file mode 100644 index 0000000..5c0e152 --- /dev/null +++ b/tests/Console/ShowModelCommandTest.php @@ -0,0 +1,26 @@ +artisan('model:show') + ->assertFailed(); +})->throws('Not enough arguments (missing: "model")'); + +test('model:show \\TestSetup\\Models\\Character', function () { + $this->artisan('model:show', ['model' => '\\TestSetup\\Models\\Character']) + ->expectsOutputToContain('arangodb') + ->expectsOutputToContain('characters') + ->expectsOutputToContain('traditional') + ->expectsOutputToContain('computed') + ->assertSuccessful(); +}); + +test('model:show \\TestSetup\\Models\\Character --json', function () { + $this->artisan('model:show', ['model' => '\\TestSetup\\Models\\Character']) + ->expectsOutputToContain('arangodb') + ->expectsOutputToContain('characters') + ->expectsOutputToContain('traditional') + ->expectsOutputToContain('computed') + ->assertSuccessful(); +}); From 1129c4fe21b5607d3347ceaadc2b7a07f508af78 Mon Sep 17 00:00:00 2001 From: Bas Date: Sun, 15 Dec 2024 19:50:01 +0100 Subject: [PATCH 19/34] chore: qa improvements --- src/Query/Concerns/BuildsGroups.php | 6 ++---- src/Query/Concerns/CompilesColumns.php | 4 ++++ src/Query/Concerns/CompilesUnions.php | 6 ++++++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Query/Concerns/BuildsGroups.php b/src/Query/Concerns/BuildsGroups.php index 45922ab..7607ec3 100644 --- a/src/Query/Concerns/BuildsGroups.php +++ b/src/Query/Concerns/BuildsGroups.php @@ -160,9 +160,7 @@ public function forNestedWhere($aliases = []) */ public function havingNested(Closure $callback, $boolean = 'and') { - $callback($query = $this->forNestedWhere($this->groups)); - - + $callback($query = $this->forNestedWhere($this->groups ?? [])); return $this->addNestedHavingQuery($query, $boolean); } @@ -176,7 +174,7 @@ public function havingNested(Closure $callback, $boolean = 'and') */ public function addNestedHavingQuery($query, $boolean = 'and') { - if (count($query->havings)) { + if (count($query->havings ?? [])) { $type = 'Nested'; $this->havings[] = compact('type', 'query', 'boolean'); diff --git a/src/Query/Concerns/CompilesColumns.php b/src/Query/Concerns/CompilesColumns.php index 40cd8cd..ca93959 100644 --- a/src/Query/Concerns/CompilesColumns.php +++ b/src/Query/Concerns/CompilesColumns.php @@ -289,6 +289,10 @@ protected function mergeJoinResults(IlluminateQueryBuilder $query, $returnDocs = { assert($query instanceof Builder); + if (!is_array($query->joins)) { + return $returnDocs; + } + foreach ($query->joins as $join) { $tableAlias = $query->getTableAlias($join->table); diff --git a/src/Query/Concerns/CompilesUnions.php b/src/Query/Concerns/CompilesUnions.php index 8bd5286..b7ff4b7 100644 --- a/src/Query/Concerns/CompilesUnions.php +++ b/src/Query/Concerns/CompilesUnions.php @@ -18,13 +18,19 @@ trait CompilesUnions */ protected function compileUnions(IlluminateBuilder $query, $firstQuery = '') { + if (!is_array($query->unions)) { + return ''; + } + $unionResultsId = 'union' . $query->getQueryId() . 'Results'; $unionDocId = 'union' . $query->getQueryId() . 'Result'; $query->registerTableAlias($unionResultsId, $unionDocId); $firstQuery = $this->wrapSubquery($firstQuery); + $unions = ''; + foreach ($query->unions as $union) { $prefix = ($unions !== '') ? $unions : $firstQuery; $unions = $this->compileUnion($union, $prefix); From af0ada1e75ce529c2e3ddbd3959181727dace83f Mon Sep 17 00:00:00 2001 From: Bas Date: Sun, 15 Dec 2024 20:01:47 +0100 Subject: [PATCH 20/34] fix: fixed table 'schema' array key check --- src/Testing/DatabaseTruncation.php | 41 ++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/Testing/DatabaseTruncation.php b/src/Testing/DatabaseTruncation.php index e1c41e2..272de56 100644 --- a/src/Testing/DatabaseTruncation.php +++ b/src/Testing/DatabaseTruncation.php @@ -4,7 +4,9 @@ namespace LaravelFreelancerNL\Aranguent\Testing; +use Illuminate\Database\ConnectionInterface; use Illuminate\Foundation\Testing\DatabaseTruncation as IlluminateDatabaseTruncation; +use Illuminate\Support\Collection; use LaravelFreelancerNL\Aranguent\Testing\Concerns\CanConfigureMigrationCommands; trait DatabaseTruncation @@ -47,4 +49,43 @@ protected function tableExistsIn(array $table, array $tables): bool ? ! empty(array_intersect([$table['name'], $table['schema'] . '.' . $table['name']], $tables)) : in_array($table['name'], $tables); } + + /** + * Truncate the database tables for the given database connection. + * + * @param \Illuminate\Database\ConnectionInterface $connection + * @param string|null $name + * @return void + */ + protected function truncateTablesForConnection(ConnectionInterface $connection, ?string $name): void + { + $dispatcher = $connection->getEventDispatcher(); + + $connection->unsetEventDispatcher(); + + (new Collection($this->getAllTablesForConnection($connection, $name))) + ->when( + $this->tablesToTruncate($connection, $name), + function (Collection $tables, array $tablesToTruncate) { + return $tables->filter(fn(array $table) => $this->tableExistsIn($table, $tablesToTruncate)); + }, + function (Collection $tables) use ($connection, $name) { + $exceptTables = $this->exceptTables($connection, $name); + return $tables->filter(fn(array $table) => ! $this->tableExistsIn($table, $exceptTables)); + }, + ) + ->each(function (array $table) use ($connection) { + $connection->withoutTablePrefix(function ($connection) use ($table) { + $table = $connection->table( + isset($table['schema']) ? $table['schema'] . '.' . $table['name'] : $table['name'], + ); + if ($table->exists()) { + $table->truncate(); + } + }); + }); + + $connection->setEventDispatcher($dispatcher); + } + } From 470e7e5ddfd674d850a999717ad1e0ab15328674 Mon Sep 17 00:00:00 2001 From: Bas Date: Sun, 15 Dec 2024 21:03:40 +0100 Subject: [PATCH 21/34] chore: renamed command traits for clarity --- src/Schema/Blueprint.php | 14 ++++++++------ .../Concerns/{Columns.php => ColumnCommands.php} | 2 +- .../Concerns/{Indexes.php => IndexCommands.php} | 2 +- .../Concerns/{Tables.php => TableCommands.php} | 2 +- 4 files changed, 11 insertions(+), 9 deletions(-) rename src/Schema/Concerns/{Columns.php => ColumnCommands.php} (98%) rename src/Schema/Concerns/{Indexes.php => IndexCommands.php} (99%) rename src/Schema/Concerns/{Tables.php => TableCommands.php} (98%) diff --git a/src/Schema/Blueprint.php b/src/Schema/Blueprint.php index 4d1eddc..ad80daf 100644 --- a/src/Schema/Blueprint.php +++ b/src/Schema/Blueprint.php @@ -9,9 +9,9 @@ use Illuminate\Support\Fluent; use Illuminate\Support\Traits\Macroable; use LaravelFreelancerNL\Aranguent\Connection; -use LaravelFreelancerNL\Aranguent\Schema\Concerns\Columns; -use LaravelFreelancerNL\Aranguent\Schema\Concerns\Indexes; -use LaravelFreelancerNL\Aranguent\Schema\Concerns\Tables; +use LaravelFreelancerNL\Aranguent\Schema\Concerns\ColumnCommands; +use LaravelFreelancerNL\Aranguent\Schema\Concerns\IndexCommands; +use LaravelFreelancerNL\Aranguent\Schema\Concerns\TableCommands; /** * Class Blueprint. @@ -28,9 +28,9 @@ class Blueprint { use Macroable; - use Tables; - use Columns; - use Indexes; + use TableCommands; + use ColumnCommands; + use IndexCommands; /** * The connection that is used by the blueprint. @@ -276,6 +276,8 @@ public function __call($method, $args = []) 'unsignedTinyInteger', 'uuid', 'year', ]; + $keyMethods = ['bigIncrements', 'increments', 'mediumIncrements', ]; + if (in_array($method, $columnMethods)) { if (isset($args[0]) && is_string($args[0])) { $this->columns[] = $args[0]; diff --git a/src/Schema/Concerns/Columns.php b/src/Schema/Concerns/ColumnCommands.php similarity index 98% rename from src/Schema/Concerns/Columns.php rename to src/Schema/Concerns/ColumnCommands.php index 0350d1c..f49b500 100644 --- a/src/Schema/Concerns/Columns.php +++ b/src/Schema/Concerns/ColumnCommands.php @@ -6,7 +6,7 @@ use Illuminate\Support\Fluent; -trait Columns +trait ColumnCommands { /** * Indicate that the given attributes should be renamed. diff --git a/src/Schema/Concerns/Indexes.php b/src/Schema/Concerns/IndexCommands.php similarity index 99% rename from src/Schema/Concerns/Indexes.php rename to src/Schema/Concerns/IndexCommands.php index 7171c40..ab87a9b 100644 --- a/src/Schema/Concerns/Indexes.php +++ b/src/Schema/Concerns/IndexCommands.php @@ -5,7 +5,7 @@ use ArangoClient\Exceptions\ArangoException; use Illuminate\Support\Fluent; -trait Indexes +trait IndexCommands { use HandlesIndexNaming; diff --git a/src/Schema/Concerns/Tables.php b/src/Schema/Concerns/TableCommands.php similarity index 98% rename from src/Schema/Concerns/Tables.php rename to src/Schema/Concerns/TableCommands.php index dfecfd0..8a769a5 100644 --- a/src/Schema/Concerns/Tables.php +++ b/src/Schema/Concerns/TableCommands.php @@ -6,7 +6,7 @@ use Illuminate\Support\Fluent; -trait Tables +trait TableCommands { /** * Indicate that the table needs to be created. From 2a9956e493be690ba15c76705e61ba45457ddc83 Mon Sep 17 00:00:00 2001 From: Deploy Date: Wed, 25 Dec 2024 15:01:03 +0100 Subject: [PATCH 22/34] test: added tests --- ...019_11_15_000000_create_taggables_table.php | 6 +++++- tests/Console/ShowModelCommandTest.php | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/TestSetup/Database/Migrations/2019_11_15_000000_create_taggables_table.php b/TestSetup/Database/Migrations/2019_11_15_000000_create_taggables_table.php index ea23729..6a023a6 100644 --- a/TestSetup/Database/Migrations/2019_11_15_000000_create_taggables_table.php +++ b/TestSetup/Database/Migrations/2019_11_15_000000_create_taggables_table.php @@ -16,7 +16,11 @@ public function up() { Schema::create('taggables', function (Blueprint $collection) { // - }); + }, [ + 'keyOptions' => [ + 'type' => 'padded', + ], + ]); } /** diff --git a/tests/Console/ShowModelCommandTest.php b/tests/Console/ShowModelCommandTest.php index 5c0e152..c1e2e28 100644 --- a/tests/Console/ShowModelCommandTest.php +++ b/tests/Console/ShowModelCommandTest.php @@ -24,3 +24,21 @@ ->expectsOutputToContain('computed') ->assertSuccessful(); }); + +test('model:show \\TestSetup\\Models\\Child', function () { + $this->artisan('model:show', ['model' => '\\TestSetup\\Models\\Child']) + ->expectsOutputToContain('arangodb') + ->expectsOutputToContain('children') + ->expectsOutputToContain('traditional') + ->doesntExpectOutput('computed') + ->assertSuccessful(); +}); + +test('model:show \\TestSetup\\Models\\House', function () { + $this->artisan('model:show', ['model' => '\\TestSetup\\Models\\House']) + ->expectsOutputToContain('arangodb') + ->expectsOutputToContain('houses') + ->expectsOutputToContain('traditional') + ->doesntExpectOutput('computed') + ->assertSuccessful(); +}); From b18361a772556cbc62fcf171639afc98bdf3898c Mon Sep 17 00:00:00 2001 From: Deploy Date: Wed, 25 Dec 2024 15:50:37 +0100 Subject: [PATCH 23/34] feat!: expanded key generator selection --- composer.json | 4 +- config/arangodb.php | 13 +++ docs/key-options.md | 55 +++++++++++ src/Connection.php | 5 +- src/Schema/Blueprint.php | 42 +++++---- src/Schema/Concerns/TableCommands.php | 48 +++++++--- tests/Schema/TableKeyTest.php | 128 ++++++++++++++++++++++++++ tests/Schema/TableTest.php | 56 +---------- 8 files changed, 263 insertions(+), 88 deletions(-) create mode 100644 docs/key-options.md create mode 100644 tests/Schema/TableKeyTest.php diff --git a/composer.json b/composer.json index ac78cdd..43b3f49 100644 --- a/composer.json +++ b/composer.json @@ -24,8 +24,8 @@ "require": { "php": "^8.2", "ext-json": "*", - "composer/composer": "^2.7.0", - "laravel-freelancer-nl/arangodb-php-client": "^2.7.0", + "composer/composer": "^2.8.0", + "laravel-freelancer-nl/arangodb-php-client": "^2.8.0", "laravel-freelancer-nl/fluentaql": "^2.0", "laravel/framework": "^11.0", "spatie/laravel-data": "^4.4.0", diff --git a/config/arangodb.php b/config/arangodb.php index 6f8dc5c..3474d20 100644 --- a/config/arangodb.php +++ b/config/arangodb.php @@ -5,9 +5,22 @@ return [ 'datetime_format' => 'Y-m-d\TH:i:s.vp', 'schema' => [ + /* + * @see https://docs.arangodb.com/stable/develop/http-api/collections/#create-a-collection_body_keyOptions_allowUserKeys + */ 'keyOptions' => [ 'allowUserKeys' => true, 'type' => 'traditional', ], + 'key_handling' => [ + 'prioritize_configured_key_type' => false, + 'use_traditional_over_autoincrement' => true, + ], + // Key type prioritization takes place in the following order: + // 1: table config within the migration file (this always takes priority) + // 2: The id column methods such as id() and ...Increments() methods in the migration file + // 3: The configured key type above. + // The order of 2 and 3 can be swapped; in which case the configured key takes priority over column methods. + // These settings are merged, individual keyOptions can be overridden in this way. ], ]; diff --git a/docs/key-options.md b/docs/key-options.md new file mode 100644 index 0000000..d46fc9d --- /dev/null +++ b/docs/key-options.md @@ -0,0 +1,55 @@ +# Key generator options +Tables in ArangoDB can be created using one of the available key generators. You can read up about them +[here](https://docs.arangodb.com/stable/concepts/data-structure/documents/#document-keys) +and [here](https://docs.arangodb.com/stable/develop/http-api/collections/#create-a-collection_body_keyOptions). + +The following assumes you have knowledge about ArangoDB keys as can be obtained through the above links. + +## Column defined key generators +Laravel has several column methods which can be used to set a primary key. If the given field is equal to: +'id', '_key' or '_id', the key generator will be set according to the mapping below. + +If these column methods are not found, or not called on these fields, the configured default generator is used. +You can also ignore the column methods by setting the config value +'arangodb.schema.key_handling.prioritize_configured_key_type' to true. + +By default, we map the key methods to the following ArangoDB key generators: + +| Laravel column method | ArangoDB key generator | +|:----------------------|:-----------------------| +| autoIncrement() | traditional | +| id() | traditional | +| increments('id') | traditional | +| smallIncrements | traditional | +| bigIncrements | traditional | +| mediumIncrements | traditional | +| uuid(id) | uuid | +| ulid(id) | _n/a_ | + +## Traditional vs autoincrement key generators +Even though ArangoDB has an autoincrement key generator we don't use it by default as it is not cluster safe. +The traditional key generator is similar to autoincrement: it is cluster safe although there may be gaps between +the _key increases. + +If you want the column methods to set the generator to autoincrement you can override the default behaviour by setting +the config value 'arangodb.schema.key_handling.use_traditional_over_autoincrement' to false. +In which case any given offset in the 'from' method is also used. + +## ulid +There is no ulid key generator in ArangoDB. The 'padded' generator may be used if you want +a lexigraphical sort order. You can do so by setting it in the config as the default key, and using configured keys only. +Or by setting it within the migration in the table options. + +## Table option key generators +You can set the key options for the table in the migration. This overrides both the default key options and the one defined by column methods. + +``` + Schema::create('taggables', function (Blueprint $collection) { + // + }, [ + 'keyOptions' => [ + 'type' => 'padded', + ], + ]); + ``` + diff --git a/src/Connection.php b/src/Connection.php index b191382..b60a5c9 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -27,7 +27,10 @@ class Connection extends IlluminateConnection use ManagesTransactions; use RunsQueries; - protected ?ArangoClient $arangoClient = null; + /** + * @var ArangoClient|null + */ + protected $arangoClient; /** * The ArangoDB driver name. diff --git a/src/Schema/Blueprint.php b/src/Schema/Blueprint.php index ad80daf..a039b35 100644 --- a/src/Schema/Blueprint.php +++ b/src/Schema/Blueprint.php @@ -276,21 +276,15 @@ public function __call($method, $args = []) 'unsignedTinyInteger', 'uuid', 'year', ]; - $keyMethods = ['bigIncrements', 'increments', 'mediumIncrements', ]; - if (in_array($method, $columnMethods)) { if (isset($args[0]) && is_string($args[0])) { $this->columns[] = $args[0]; } } - $autoIncrementMethods = ['increments', 'autoIncrement']; - if (in_array($method, $autoIncrementMethods)) { - $this->setKeyGenerator('autoincrement'); - } - - if ($method === 'uuid') { - $this->setKeyGenerator('uuid'); + $keyMethods = ['autoIncrement', 'bigIncrements', 'increments', 'mediumIncrements', 'tinyIncrements', 'uuid']; + if (in_array($method, $keyMethods)) { + $this->handleKeyCommands($method, $args); } $this->ignoreMethod($method); @@ -298,14 +292,6 @@ public function __call($method, $args = []) return $this; } - protected function setKeyGenerator(string $generator = 'traditional'): void - { - $column = end($this->columns); - if ($column === '_key' || $column === 'id') { - $this->keyGenerator = $generator; - } - } - protected function ignoreMethod(string $method) { $info = []; @@ -320,4 +306,26 @@ public function renameIdField(mixed $fields) return $value === 'id' ? '_key' : $value; }, $fields); } + + /** + * @param mixed[] $options + * @return mixed[] + */ + protected function setKeyOptions($tableOptions) + { + $configuredKeyOptions = config('arangodb.schema.keyOptions'); + + $columnOptions = []; + $columnOptions['type'] = $this->keyGenerator; + + $mergedKeyOptions = (config('arangodb.schema.key_handling.prioritize_configured_key_type')) + ? array_merge($columnOptions, $configuredKeyOptions, $tableOptions) + : array_merge($configuredKeyOptions, $columnOptions, $tableOptions); + + if ($mergedKeyOptions['type'] === 'autoincrement' && $this->incrementOffset !== 0) { + $mergedKeyOptions['offset'] = $this->incrementOffset; + } + + return $mergedKeyOptions; + } } diff --git a/src/Schema/Concerns/TableCommands.php b/src/Schema/Concerns/TableCommands.php index 8a769a5..8212efd 100644 --- a/src/Schema/Concerns/TableCommands.php +++ b/src/Schema/Concerns/TableCommands.php @@ -17,35 +17,55 @@ trait TableCommands public function create($options = []) { $parameters = []; - $parameters['options'] = array_merge( - [ - 'keyOptions' => config('arangodb.schema.keyOptions'), - ], - $options, - ); + $parameters['options'] = $options; $parameters['explanation'] = "Create '{$this->table}' table."; $parameters['handler'] = 'table'; return $this->addCommand('create', $parameters); } - public function executeCreateCommand($command) + /** + * @param string $command + * @param mixed[] $args + * @return void + */ + public function handleKeyCommands($command, $args) { - if ($this->connection->pretending()) { - $this->connection->logQuery('/* ' . $command->explanation . " */\n", []); + $acceptedKeyFields = ['id', '_id', '_key']; + + $columns = ($command === 'autoIncrement') ? end($this->columns) : $args; + $columns = (is_array($columns)) ? $columns : [$columns]; + if (count($columns) !== 1 || ! in_array($columns[0], $acceptedKeyFields)) { return; } - $options = $command->options; - if ($this->keyGenerator !== 'traditional') { - $options['keyOptions']['type'] = $this->keyGenerator; + if ($command === 'uuid') { + $this->keyGenerator = 'uuid'; + + return; } - if ($this->keyGenerator === 'autoincrement' && $this->incrementOffset !== 0) { - $options['keyOptions']['offset'] = $this->incrementOffset; + if (config('arangodb.schema.key_handling.use_traditional_over_autoincrement') === false) { + $this->keyGenerator = 'autoincrement'; + + return; } + $this->keyGenerator = 'traditional'; + } + + public function executeCreateCommand($command) + { + if ($this->connection->pretending()) { + $this->connection->logQuery('/* ' . $command->explanation . " */\n", []); + + return; + } + + $options = $command->options; + $options['keyOptions'] = $this->setKeyOptions($options['keyOptions'] ?? []); + if (!$this->schemaManager->hasCollection($this->table)) { $this->schemaManager->createCollection($this->table, $options); } diff --git a/tests/Schema/TableKeyTest.php b/tests/Schema/TableKeyTest.php new file mode 100644 index 0000000..4248b0a --- /dev/null +++ b/tests/Schema/TableKeyTest.php @@ -0,0 +1,128 @@ +getSchemaBuilder(); + + $schema->create('white_walkers', function (Blueprint $table) use (& $creating) {}); + + $schemaManager = $this->connection->getArangoClient()->schema(); + + $collectionProperties = $schemaManager->getCollectionProperties('white_walkers'); + + expect($collectionProperties->keyOptions->type)->toBe('traditional'); + + Schema::drop('white_walkers'); +}); + +test('creating table with different default key generator', function () { + Config::set('arangodb.schema.keyOptions.type', 'padded'); + $schema = DB::connection()->getSchemaBuilder(); + + $schema->create('white_walkers', function (Blueprint $table) use (& $creating) {}); + + $schemaManager = $this->connection->getArangoClient()->schema(); + + $collectionProperties = $schemaManager->getCollectionProperties('white_walkers'); + + expect($collectionProperties->keyOptions->type)->toBe('padded'); + + Schema::drop('white_walkers'); +}); + +test('creating table with autoincrement key generator', function () { + Config::set('arangodb.schema.key_handling.use_traditional_over_autoincrement', false); + + $schema = DB::connection()->getSchemaBuilder(); + + $schema->create('white_walkers', function (Blueprint $table) use (& $creating) { + $table->increments('id'); + }); + + $schemaManager = $this->connection->getArangoClient()->schema(); + + $collectionProperties = $schemaManager->getCollectionProperties('white_walkers'); + + expect($collectionProperties->keyOptions->type)->toBe('autoincrement'); + + Schema::drop('white_walkers'); + Config::set('arangodb.schema.key_handling.use_traditional_over_autoincrement', true); +}); + +test('creating table with autoIncrement offset', function () { + Config::set('arangodb.schema.key_handling.use_traditional_over_autoincrement', false); + + $schema = DB::connection()->getSchemaBuilder(); + + $schema->create('white_walkers', function (Blueprint $table) use (& $creating) { + $table->string('id')->autoIncrement()->from(5); + }); + + $schemaManager = $this->connection->getArangoClient()->schema(); + + $collectionProperties = $schemaManager->getCollectionProperties('white_walkers'); + + expect($collectionProperties->keyOptions->type)->toBe('autoincrement'); + expect($collectionProperties->keyOptions->offset)->toBe(5); + + Schema::drop('white_walkers'); +}); + +test('create table with uuid key generator', function () { + $schema = DB::connection()->getSchemaBuilder(); + + $schema->create('white_walkers', function (Blueprint $table) use (& $creating) { + $table->uuid('id'); + }); + + $schemaManager = $this->connection->getArangoClient()->schema(); + + $collectionProperties = $schemaManager->getCollectionProperties('white_walkers'); + + expect($collectionProperties->keyOptions->type)->toBe('uuid'); + + Schema::drop('white_walkers'); +}); + +test('table options override column key generator', function () { + $schema = DB::connection()->getSchemaBuilder(); + + $schema->create('white_walkers', function (Blueprint $table) use (& $creating) { + $table->uuid('id'); + }, [ + 'keyOptions' => [ + 'type' => 'padded', + ], + ]); + + $schemaManager = $this->connection->getArangoClient()->schema(); + + $collectionProperties = $schemaManager->getCollectionProperties('white_walkers'); + + expect($collectionProperties->keyOptions->type)->toBe('padded'); + + Schema::drop('white_walkers'); +}); + +test('table options override default key generator', function () { + $schema = DB::connection()->getSchemaBuilder(); + + $schema->create('white_walkers', function (Blueprint $table) use (& $creating) {}, [ + 'keyOptions' => [ + 'type' => 'padded', + 'allowUserKeys' => false, + ], + ]); + + $schemaManager = $this->connection->getArangoClient()->schema(); + + $collectionProperties = $schemaManager->getCollectionProperties('white_walkers'); + + expect($collectionProperties->keyOptions->type)->toBe('padded'); + expect($collectionProperties->keyOptions->allowUserKeys)->toBeFalse(); + + Schema::drop('white_walkers'); +}); diff --git a/tests/Schema/TableTest.php b/tests/Schema/TableTest.php index 05af1ea..d01b32b 100644 --- a/tests/Schema/TableTest.php +++ b/tests/Schema/TableTest.php @@ -3,70 +3,18 @@ use LaravelFreelancerNL\Aranguent\Exceptions\QueryException; use LaravelFreelancerNL\Aranguent\Schema\Blueprint; -test('creating table with increments', function () { +test('creating table', function () { $schema = DB::connection()->getSchemaBuilder(); - $schema->create('white_walkers', function (Blueprint $table) use (& $creating) { - $table->increments('id'); - }); + $schema->create('white_walkers', function (Blueprint $table) use (& $creating) {}); $schemaManager = $this->connection->getArangoClient()->schema(); $collectionProperties = $schemaManager->getCollectionProperties('white_walkers'); - expect($collectionProperties->keyOptions->type)->toBe('autoincrement'); - - Schema::drop('white_walkers'); -}); - -test('creating table with autoIncrement', function () { - $schema = DB::connection()->getSchemaBuilder(); - - $schema->create('white_walkers', function (Blueprint $table) use (& $creating) { - $table->string('id')->autoIncrement(); - }); - - $schemaManager = $this->connection->getArangoClient()->schema(); - - $collectionProperties = $schemaManager->getCollectionProperties('white_walkers'); - - expect($collectionProperties->keyOptions->type)->toBe('autoincrement'); - - Schema::drop('white_walkers'); -}); -test('creating table with autoIncrement offset', function () { - $schema = DB::connection()->getSchemaBuilder(); - - $schema->create('white_walkers', function (Blueprint $table) use (& $creating) { - $table->string('id')->autoIncrement()->from(5); - }); - - $schemaManager = $this->connection->getArangoClient()->schema(); - - $collectionProperties = $schemaManager->getCollectionProperties('white_walkers'); - - expect($collectionProperties->keyOptions->type)->toBe('autoincrement'); - Schema::drop('white_walkers'); }); -test('create table with uuid key generator', function () { - $schema = DB::connection()->getSchemaBuilder(); - - $schema->create('white_walkers', function (Blueprint $table) use (& $creating) { - $table->uuid('id'); - }); - - $schemaManager = $this->connection->getArangoClient()->schema(); - - $collectionProperties = $schemaManager->getCollectionProperties('white_walkers'); - - expect($collectionProperties->keyOptions->type)->toBe('uuid'); - - Schema::drop('white_walkers'); -}); - - test('hasTable', function () { expect(Schema::hasTable('locations'))->toBeTrue(); expect(Schema::hasTable('dummy'))->toBeFalse(); From 9a97b557f00fef341f0cb606d0ff4efdfbe1ee7f Mon Sep 17 00:00:00 2001 From: Deploy Date: Sat, 28 Dec 2024 16:07:44 +0100 Subject: [PATCH 24/34] feat: Added lateralJoin support --- src/Query/Concerns/BuildsJoins.php | 24 +++++++++++++++ src/Query/Concerns/CompilesColumns.php | 2 +- src/Query/Concerns/CompilesJoins.php | 1 + tests/Query/JoinTest.php | 41 ++++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/Query/Concerns/BuildsJoins.php b/src/Query/Concerns/BuildsJoins.php index 989a4cd..d8cbe67 100644 --- a/src/Query/Concerns/BuildsJoins.php +++ b/src/Query/Concerns/BuildsJoins.php @@ -182,4 +182,28 @@ protected function newJoinClause(IlluminateQueryBuilder $parentQuery, $type, $ta return new JoinClause($parentQuery, $type, $table); } + /** + * Add a lateral join clause to the query. + * + * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder<*>|string $query + * @param string $as + * @param string $type + * @return $this + */ + public function joinLateral($query, string $as, string $type = 'inner') + { + assert($query instanceof Builder); + + $query->importTableAliases($this); + $query->importTableAliases([$as => $as]); + $this->importTableAliases($query); + + [$query] = $this->createSub($query); + + $expression = $query . ' as ' . $this->grammar->wrapTable($as); + + $this->joins[] = $this->newJoinLateralClause($this, $type, new Expression($expression)); + + return $this; + } } diff --git a/src/Query/Concerns/CompilesColumns.php b/src/Query/Concerns/CompilesColumns.php index ca93959..9f81188 100644 --- a/src/Query/Concerns/CompilesColumns.php +++ b/src/Query/Concerns/CompilesColumns.php @@ -172,7 +172,6 @@ protected function normalizeColumnReferences(IlluminateQueryBuilder $query, stri $references = explode('.', $column); - $tableAlias = $query->getTableAlias($references[0]); if (isset($tableAlias)) { @@ -184,6 +183,7 @@ protected function normalizeColumnReferences(IlluminateQueryBuilder $query, stri array_unshift($references, $tableAlias); } + // geen tableAlias, table is parent...waarom geen tableAlias? if ($tableAlias === null && array_key_exists($table, $query->tableAliases)) { array_unshift($references, $query->tableAliases[$table]); } diff --git a/src/Query/Concerns/CompilesJoins.php b/src/Query/Concerns/CompilesJoins.php index 56b4ae7..264dda5 100644 --- a/src/Query/Concerns/CompilesJoins.php +++ b/src/Query/Concerns/CompilesJoins.php @@ -29,6 +29,7 @@ public function extractTableAndAlias(Builder $query, $join): array return [$table, $alias]; } + } $table = (string) $this->wrapTable($join->table); diff --git a/tests/Query/JoinTest.php b/tests/Query/JoinTest.php index f9abb7b..cc6f96d 100644 --- a/tests/Query/JoinTest.php +++ b/tests/Query/JoinTest.php @@ -110,3 +110,44 @@ 'coordinate', ]); }); + +test('joinLateral', function () { + $controlledLocations = DB::table('locations') + ->whereColumn('locations.led_by', '==', 'characters.id') + ->limit(3); + + $leadingLadies = DB::table('characters') + ->joinLateral( + $controlledLocations, + 'controlled_territory', + ) + ->orderBy('name') + ->get(); + + expect($leadingLadies)->toHaveCount(6); +}); + + +test('joinLateral with selected fields', function () { + $controlledLocations = DB::table('locations') + ->select('id as location_id', 'name as location_name') + ->whereColumn('locations.led_by', '==', 'characters.id') + ->orderBy('name') + ->limit(3); + + $leadingLadies = DB::table('characters') + ->select('id', 'name', 'controlled_territory.location_name as territory_name') + ->joinLateral( + $controlledLocations, + 'controlled_territory', + ) + ->orderBy('name') + ->get(); + + expect($leadingLadies)->toHaveCount(6); + + expect(($leadingLadies[1])->name)->toBe('Cersei'); + expect(($leadingLadies[2])->name)->toBe('Daenerys'); + expect(($leadingLadies[2])->territory_name)->toBe('Astapor'); + expect(($leadingLadies[5])->name)->toBe('Sansa'); +}); From e6a347727a9605c16745adf8119d13b44f1734ad Mon Sep 17 00:00:00 2001 From: Deploy Date: Sun, 29 Dec 2024 00:18:53 +0100 Subject: [PATCH 25/34] docs: updated list with confirmed compatible methods --- docs/compatibility-list.md | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/docs/compatibility-list.md b/docs/compatibility-list.md index 5cedd44..44fc7cc 100644 --- a/docs/compatibility-list.md +++ b/docs/compatibility-list.md @@ -65,7 +65,7 @@ Union orders / Union aggregates / Union groupBy Expression / raw ### Joins -crossJoin / join / joinSub? / leftJoin / leftJoinSub? +crossJoin / join / joinSub / lateralJoin / leftJoin / leftJoinSub #### Unsupported join clauses rightJoin / rightJoinSub / joinWhere? @@ -103,7 +103,7 @@ limit / offset / take / skip when ### Insert statements -insert / insertOrIgnore / insertUsing / insertGetId +insert / insertOrIgnore / insertUsing / insertOrIgnoreUsing / insertGetId ### Update statements update / updateOrInsert / upsert / @@ -183,14 +183,14 @@ updateExistingPivot / ## Artisan commands The following database-related artisan commands are supported: -make:model / db / db:wipe / -make:migration / migrate:install / migrate / +db / db:monitor / db:show / db:table / db:wipe / +make:migration / make:model / migrate:install / migrate / migrate:fresh / migrate:refresh / migrate:reset / migrate:rollback / migrate:status / convert:migrations The following database-related artisan commands are NOT support at this time: -db:monitor / db:show / db:table / schema:dump +schema:dump ## Testing @@ -207,16 +207,6 @@ castAsJson (dummy method) ## Database connection escape -## Console commands -The following database related console commands are compatible with vanilla Laravel: - -db:monitor / db:seed / db:wipe -make:migrate / migrate / -migrate:fresh / migrate:install / migrate:refresh / migrate:reset / migrate:rollback / migrate:status - -### Incompatible console commands -db:show / db:table - ## Known incompatibilities Not all features can be made compatible. Known issues are listed below: @@ -241,8 +231,9 @@ These methods don't work as ArangoDB requires you to declare the locking mechani ### Raw SQL Any raw SQL needs to be replaced by raw AQL. -### Separate read and write connections -Aranguent currently doesn't support the combination of a separate read and write connection +### Database replication: separate read and write connections +ArangoDB offers a cluster setup for high availability instead of replication and as such +doesn't support the combination of a separate read and write connection. ### Transactions [At the beginning of a transaction you must declare collections that are used in (write) statements.](transactions.md) From 50dee02ebe68c734eb85b8eacb8ef5e7251de7d2 Mon Sep 17 00:00:00 2001 From: Deploy Date: Sun, 29 Dec 2024 00:19:12 +0100 Subject: [PATCH 26/34] feat: Added support for insertOrIgnoreUsing --- .../Concerns/CompilesDataManipulations.php | 35 +++++++++++++++++++ tests/Query/InsertTest.php | 27 ++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/src/Query/Concerns/CompilesDataManipulations.php b/src/Query/Concerns/CompilesDataManipulations.php index 4c142b0..b7dc0f5 100644 --- a/src/Query/Concerns/CompilesDataManipulations.php +++ b/src/Query/Concerns/CompilesDataManipulations.php @@ -122,6 +122,41 @@ public function compileInsertUsing(IlluminateQueryBuilder $query, array $columns return $aql; } + /** + * Compile an insert statement using a subquery into SQL. + * + * @param IlluminateQueryBuilder $query + * @param array $columns + * @param string $sql + * @return string + */ + public function compileInsertOrIgnoreUsing(IlluminateQueryBuilder $query, array $columns, string $sql) + { + $table = $this->wrapTable($query->from); + + $insertDoc = ''; + if (empty($columns) || $columns === ['*']) { + $insertDoc = 'docDoc'; + } + + + if ($insertDoc === '') { + $insertValues = []; + foreach ($columns as $column) { + $insertValues[$column] = $this->normalizeColumnReferences($query, $column, 'docs'); + } + $insertDoc = $this->generateAqlObject($insertValues); + } + + $aql = /** @lang AQL */ 'LET docs = ' . $sql + . ' FOR docDoc IN docs' + . ' INSERT ' . $insertDoc . ' INTO ' . $table + . ' OPTIONS { ignoreErrors: true }' + . ' RETURN NEW._key'; + + return $aql; + } + /** * @param array $values * @return string diff --git a/tests/Query/InsertTest.php b/tests/Query/InsertTest.php index eb4b2ae..b63a096 100644 --- a/tests/Query/InsertTest.php +++ b/tests/Query/InsertTest.php @@ -136,3 +136,30 @@ expect($user->surname)->toBe('Baelish'); }); + +test('insertOrIgnoreUsing', function () { + // Let's give Baelish a user, what could possibly go wrong? Everyone trusts him... + $baelishes = DB::table('characters') + ->where('surname', 'Baelish'); + + DB::table('users')->insertOrIgnoreUsing(['name', 'surname'], $baelishes); + + $user = DB::table('users')->where("surname", "=", "Baelish")->first(); + + expect($user->surname)->toBe('Baelish'); +}); + +test("insertOrIgnoreUsing doesn't error on duplicates", function () { + // Let's give Baelish a user, what could possibly go wrong? Everyone trusts him... + $baelish = DB::table('characters') + ->where('surname', 'Baelish'); + + DB::table('users')->insertUsing(['name', 'surname'], $baelish); + + // Let's do it again. + DB::table('users')->insertOrIgnoreUsing(['name', 'surname'], $baelish); + + $user = DB::table('users')->where("surname", "=", "Baelish")->first(); + + expect($user->surname)->toBe('Baelish'); +})->only(); From 53b98751c92f0a840198a83ebbad1d5311520faa Mon Sep 17 00:00:00 2001 From: Deploy Date: Sun, 29 Dec 2024 00:23:37 +0100 Subject: [PATCH 27/34] chore: Removed only call --- tests/Query/InsertTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Query/InsertTest.php b/tests/Query/InsertTest.php index b63a096..1f599c0 100644 --- a/tests/Query/InsertTest.php +++ b/tests/Query/InsertTest.php @@ -162,4 +162,4 @@ $user = DB::table('users')->where("surname", "=", "Baelish")->first(); expect($user->surname)->toBe('Baelish'); -})->only(); +}); From 53d08fc41993b2b069081c3a5a342b94e4635645 Mon Sep 17 00:00:00 2001 From: Deploy Date: Sun, 5 Jan 2025 11:41:02 +0100 Subject: [PATCH 28/34] fix: ensure attribute property exists --- src/Console/ShowModelCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Console/ShowModelCommand.php b/src/Console/ShowModelCommand.php index 24fe004..35b6d10 100644 --- a/src/Console/ShowModelCommand.php +++ b/src/Console/ShowModelCommand.php @@ -109,7 +109,7 @@ protected function displayCli($class, $database, $table, $policy, $attributes, $ '%s %s', $attribute['name'], collect(['computed', 'increments', 'unique', 'nullable', 'fillable', 'hidden', 'appended']) - ->filter(fn($property) => $attribute[$property]) + ->filter(fn($property) => (isset($attribute[$property])) ? $attribute[$property] : false) ->map(fn($property) => sprintf('%s', $property)) ->implode(', '), )); From 5c5b109c5f622fd959b5ebddfd6d52e7fd4434e3 Mon Sep 17 00:00:00 2001 From: Deploy Date: Tue, 7 Jan 2025 23:17:22 +0100 Subject: [PATCH 29/34] feat: support union aggregates --- src/Query/Concerns/CompilesColumns.php | 46 +++++++++++++++----- src/Query/Concerns/CompilesUnions.php | 14 ++++-- src/Query/Grammar.php | 13 +++--- tests/Query/UnionTest.php | 60 ++++++++++++++++++++++++++ 4 files changed, 113 insertions(+), 20 deletions(-) diff --git a/src/Query/Concerns/CompilesColumns.php b/src/Query/Concerns/CompilesColumns.php index 9f81188..50c3966 100644 --- a/src/Query/Concerns/CompilesColumns.php +++ b/src/Query/Concerns/CompilesColumns.php @@ -11,6 +11,39 @@ trait CompilesColumns { + /** + * @param mixed[] $returnAttributes + * @param mixed[] $returnDocs + * @param Builder $query + * @return mixed[] + */ + public function processEmptyReturnValues(array $returnAttributes, array $returnDocs, Builder $query): array + { + if (empty($returnAttributes) && empty($returnDocs)) { + $returnDocs[] = (string) $query->getTableAlias($query->from); + + if ($query->joins !== null) { + $returnDocs = $this->mergeJoinResults($query, $returnDocs); + } + } + return $returnDocs; + } + + /** + * @param Builder $query + * @param mixed[] $returnDocs + * @param mixed[] $returnAttributes + * @return mixed[] + */ + public function processAggregateReturnValues(Builder $query, array $returnDocs, array $returnAttributes): array + { + if ($query->aggregate !== null && $query->unions === null) { + $returnDocs = []; + $returnAttributes = ['aggregate' => 'aggregateResult']; + } + return [$returnDocs, $returnAttributes]; + } + /** * Compile the "select *" portion of the query. * @@ -235,19 +268,10 @@ protected function determineReturnValues(IlluminateQueryBuilder $query, $returnA assert($query instanceof Builder); // If nothing was specifically requested, we return everything. - if (empty($returnAttributes) && empty($returnDocs)) { - $returnDocs[] = (string) $query->getTableAlias($query->from); - - if ($query->joins !== null) { - $returnDocs = $this->mergeJoinResults($query, $returnDocs); - } - } + $returnDocs = $this->processEmptyReturnValues($returnAttributes, $returnDocs, $query); // Aggregate functions only return the aggregate, so we can clear out everything else. - if ($query->aggregate !== null) { - $returnDocs = []; - $returnAttributes = ['aggregate' => 'aggregateResult']; - } + list($returnDocs, $returnAttributes) = $this->processAggregateReturnValues($query, $returnDocs, $returnAttributes); // Return a single value for certain subqueries if ( diff --git a/src/Query/Concerns/CompilesUnions.php b/src/Query/Concerns/CompilesUnions.php index b7ff4b7..4870077 100644 --- a/src/Query/Concerns/CompilesUnions.php +++ b/src/Query/Concerns/CompilesUnions.php @@ -39,8 +39,6 @@ protected function compileUnions(IlluminateBuilder $query, $firstQuery = '') $aql = 'LET ' . $unionResultsId . ' = ' . $unions . ' FOR ' . $unionDocId . ' IN ' . $unionResultsId; - // Union groups - if (!empty($query->unionOrders)) { $aql .= ' ' . $this->compileOrders($query, $query->unionOrders, $unionResultsId); } @@ -53,7 +51,17 @@ protected function compileUnions(IlluminateBuilder $query, $firstQuery = '') $aql .= ' ' . $this->compileLimit($query, $query->unionLimit); } - // Union aggregates? + if ($query->aggregate !== null) { + $originalFrom = $query->from; + $query->from = $unionResultsId; + + $aql .= ' ' . $this->compileAggregate($query, $query->aggregate); + + $query->from = $originalFrom; + + return $aql . ' RETURN { `aggregate`: aggregateResult }'; + } + return $aql . ' RETURN ' . $unionDocId; } diff --git a/src/Query/Grammar.php b/src/Query/Grammar.php index 189ba82..903fdd4 100644 --- a/src/Query/Grammar.php +++ b/src/Query/Grammar.php @@ -175,6 +175,10 @@ protected function compileComponents(IlluminateQueryBuilder $query) continue; } + if ($component === 'aggregate' && $query->unions !== null) { + continue; + } + if (isset($query->$component)) { $method = 'compile' . ucfirst($component); @@ -202,7 +206,7 @@ public function compileSelect(IlluminateQueryBuilder $query) // can build the query and concatenate all the pieces together as one. $original = $query->columns; - if (empty($query->columns)) { + if (empty($query->columns) || $query->unions !== null) { $query->columns = ['*']; } @@ -216,15 +220,12 @@ public function compileSelect(IlluminateQueryBuilder $query) ), ); - // if ($query->unions && $query->aggregate) { - // return $this->compileUnionAggregate($query); - // } + $query->columns = $original; + if ($query->unions) { return $this->compileUnions($query, $aql); } - $query->columns = $original; - if ($query->groupVariables !== null) { $query->cleanGroupVariables(); } diff --git a/tests/Query/UnionTest.php b/tests/Query/UnionTest.php index d4f2ecd..43e1605 100644 --- a/tests/Query/UnionTest.php +++ b/tests/Query/UnionTest.php @@ -77,3 +77,63 @@ expect(($results->first())->name)->toBe('Robert'); expect(($results->last())->name)->toBe('Roose'); }); + +test('union aggregate average', function () { + $charactersWithoutAge = DB::table('characters') + ->where('surname', 'Stark'); + + $averageAge = DB::table('characters') + ->where('surname', 'Lannister') + ->union($charactersWithoutAge) + ->avg('age'); + + expect($averageAge)->toBe(27.375); +}); + +test('union aggregate count', function () { + $charactersWithoutAge = DB::table('characters') + ->where('surname', 'Stark'); + + $averageAge = DB::table('characters') + ->where('surname', 'Lannister') + ->union($charactersWithoutAge) + ->count(); + + expect($averageAge)->toBe(10); +}); + +test('union aggregate min', function () { + $charactersWithoutAge = DB::table('characters') + ->where('surname', 'Stark'); + + $averageAge = DB::table('characters') + ->where('surname', 'Lannister') + ->union($charactersWithoutAge) + ->min('age'); + + expect($averageAge)->toBe(10); +}); + +test('union aggregate max', function () { + $charactersWithoutAge = DB::table('characters') + ->where('surname', 'Stark'); + + $averageAge = DB::table('characters') + ->where('surname', 'Lannister') + ->union($charactersWithoutAge) + ->max('age'); + + expect($averageAge)->toBe(41); +}); + +test('union aggregate sum', function () { + $charactersWithoutAge = DB::table('characters') + ->where('surname', 'Stark'); + + $averageAge = DB::table('characters') + ->where('surname', 'Lannister') + ->union($charactersWithoutAge) + ->sum('age'); + + expect($averageAge)->toBe(219); +}); From 1935f6b3026e124868963aa8b4d19059f6d19d31 Mon Sep 17 00:00:00 2001 From: Deploy Date: Tue, 7 Jan 2025 23:17:46 +0100 Subject: [PATCH 30/34] docs: updated list of supported query methods --- docs/compatibility-list.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/compatibility-list.md b/docs/compatibility-list.md index 44fc7cc..e458dd4 100644 --- a/docs/compatibility-list.md +++ b/docs/compatibility-list.md @@ -58,9 +58,6 @@ groupByRaw ### Unions union / unionAll -#### Unsupported union clauses -Union orders / Union aggregates / Union groupBy - ### Expressions Expression / raw @@ -68,7 +65,7 @@ Expression / raw crossJoin / join / joinSub / lateralJoin / leftJoin / leftJoinSub #### Unsupported join clauses -rightJoin / rightJoinSub / joinWhere? +rightJoin / rightJoinSub / rightJoinWhere / joinWhere? / leftJoinWhere? ### Where clauses where / orWhere / whereNot / orWhereNot / whereColumn / whereExists From c6ac1e60eaccf76265e806a1830a53367b6c35ac Mon Sep 17 00:00:00 2001 From: Laravel Freelancer NL <36150929+LaravelFreelancerNL@users.noreply.github.com> Date: Sun, 19 Jan 2025 01:14:58 +0100 Subject: [PATCH 31/34] 184 support wheredoesnthaverelation methods (#187) * qa: confirmed proper functioning of several eloquent relationship query methods * fix: table alias was set to null in returndocs --------- Co-authored-by: Deploy --- docs/compatibility-list.md | 61 +++++----- src/Query/Concerns/HandlesAliases.php | 4 + tests/Eloquent/MorphToTest.php | 130 ++++++++++++++++++++ tests/Eloquent/RelationshipQueriesTest.php | 134 ++++++++++++++++++++- 4 files changed, 298 insertions(+), 31 deletions(-) diff --git a/docs/compatibility-list.md b/docs/compatibility-list.md index e458dd4..cc86823 100644 --- a/docs/compatibility-list.md +++ b/docs/compatibility-list.md @@ -111,7 +111,7 @@ update with join delete / truncate ### Debugging -dd / dump / toSql / ddRawSql / dumpRawSql / toRawSql +dd / dump / toSql / ddRawSql? / dumpRawSql / toRawSql? ## Eloquent The methods listed below are **specific** to Eloquent. @@ -120,17 +120,17 @@ in the chapter above._ ### Model CRUD all / first / firstWhere / firstOr / firstOrFail / -firstOrCreate / firstOrNew? / -find / findOr / fresh? / refresh? / -create / createOrFirst / fill / save / update / updateOrCreate / -upsert / replicate / delete / destroy / truncate / softDeletes / -trashed? / restore? / withTrashed? / forceDelete -isDirty? / isClean / wasChanged / getOriginal / -pruning / query scopes? / saveQuietly / deleteQuietly / -forceDeleteQuietly / restoreQuietly +firstOrCreate / firstOrNew / +find / findOr? / fresh / refresh / +create / createOrFirst / fill? / save / update / updateOrCreate / +upsert / replicate? / delete / destroy / truncate / softDeletes? / +trashed? / restore? / withTrashed? / forceDelete / +isDirty? / isClean? / wasChanged? / getOriginal? / +pruning? / query scopes? / saveQuietly? / deleteQuietly? / +forceDeleteQuietly? / restoreQuietly? #### Model comparison -is / isNot +is? / isNot? ### Relationships - One To One @@ -142,30 +142,35 @@ is / isNot belongsTo / belongsToMany / morphOne / morphTo / morphMany / morphMany / morphedByMany / -ofMany / latestOfMany / oldestOfMany -hasOne / hasMany / hasX / hasOneThrough / hasManyThrough / -throughX / -whereBelongsTo / -as / -withTimestamps / +ofMany? / latestOfMany? / oldestOfMany? / +has / hasOne / hasMany / hasOneThrough? / hasManyThrough? / +through? / whereBelongsTo? / #### Pivot functions -withPivot / -wherePivot / wherePivotIn /wherePivotNotIn / -wherePivotBetween / wherePivotNotBetween / -wherePivotNull / wherePivotNotNull / orderByPivot / -using +as? / withPivot? / +wherePivot? / wherePivotIn? /wherePivotNotIn? / +wherePivotBetween? / wherePivotNotBetween? / +wherePivotNull? / wherePivotNotNull? / orderByPivot? / +using? / withTimestamps? -enforceMorphMap / getMorphClass / getMorphedModel / resolveRelationUsing +enforceMorphMap? / getMorphClass? / getMorphedModel? / resolveRelationUsing? -#### Query relationships -has / orHas / whereHas / whereRelation / doesntHave / -whereDoesntHave / whereHasMorph / whereDoesntHaveMorph +#### Querying Relationship Existence +has / orHas / whereHas / orWhereHas / whereRelation / orWhereRelation / +whereMorphRelation / orWhereMorphRelation + +#### Querying Relationship Absence +doesntHave / orDoesntHave / +whereDoesntHave / orWhereDoesntHave / whereDoesntHaveRelation / orWhereDoesntHaveRelation / +whereMorphDoesntHaveRelation / orWhereMorphDoesntHaveRelation + +#### Querying Morph To Relationships +whereHasMorph / orWhereHasMorph / whereDoesntHaveMorph / orWhereDoesntHaveMorph / whereMorphedTo? / whereNotMorphedTo? #### Aggregating related models -withCount / loadCount / -withSum / loadSum / withExists / morphWithCount /loadMorphCount / -loadMorphCount +withCount / loadCount? / +withSum? / loadSum? / withExists / morphWithCount? /loadMorphCount? / +loadMorphCount? #### Eager loading with / without / withOnly / constrain / diff --git a/src/Query/Concerns/HandlesAliases.php b/src/Query/Concerns/HandlesAliases.php index 6dcd710..784a57c 100644 --- a/src/Query/Concerns/HandlesAliases.php +++ b/src/Query/Concerns/HandlesAliases.php @@ -80,6 +80,10 @@ public function getTableAlias(string|Expression $table): float|int|null|string $table = 'Expression' . spl_object_id($table); } + if ($this->isTableAlias($table)) { + return $table; + } + if (!isset($this->tableAliases[$table])) { return null; } diff --git a/tests/Eloquent/MorphToTest.php b/tests/Eloquent/MorphToTest.php index 51645cf..5674cca 100644 --- a/tests/Eloquent/MorphToTest.php +++ b/tests/Eloquent/MorphToTest.php @@ -1,5 +1,6 @@ capturable)->toBeInstanceOf(Character::class); expect($location->capturable->id)->toEqual('TheonGreyjoy'); }); + +test('whereHasMorph', function () { + $locations = Location::whereHasMorph( + 'capturable', + Character::class, + function (Builder $query) { + $query->where('_key', 'TheonGreyjoy'); + }, + )->get(); + + expect(count($locations))->toEqual(1); +}); + +test('orWhereHasMorph', function () { + $locations = Location::where(function (Builder $query) { + $query->whereHasMorph( + 'capturable', + Character::class, + function (Builder $query) { + $query->where('id', 'TheonGreyjoy'); + }, + ) + ->orWhereHasMorph( + 'capturable', + Character::class, + function (Builder $query) { + $query->where('id', 'DaenerysTargaryen'); + }, + ); + })->get(); + + expect(count($locations))->toEqual(6); +}); + +test('whereMorphRelation ', function () { + $locations = Location::whereMorphRelation( + 'capturable', + Character::class, + '_key', + 'TheonGreyjoy', + ) + ->get(); + + expect(count($locations))->toEqual(1); +}); + +test('orWhereMorphRelation', function () { + $locations = Location::where(function (Builder $query) { + $query->whereMorphRelation( + 'capturable', + Character::class, + 'id', + 'TheonGreyjoy', + ) + ->orWhereMorphRelation( + 'capturable', + Character::class, + 'id', + 'DaenerysTargaryen', + ); + })->get(); + + expect($locations->count())->toEqual(6); +}); + +test('whereDoesntHaveMorph', function () { + $locations = Location::whereDoesntHaveMorph( + 'capturable', + Character::class, + function (Builder $query) { + $query->where('id', 'DaenerysTargaryen'); + }, + )->get(); + + expect(count($locations))->toEqual(1); +}); + +test('orWhereDoesntHaveMorph', function () { + $locations = Location::where(function (Builder $query) { + $query->whereHasMorph( + 'capturable', + Character::class, + function (Builder $query) { + $query->where('alive', true); + }, + ) + ->orWhereDoesntHaveMorph( + 'capturable', + Character::class, + function (Builder $query) { + $query->where('age', '<', 20); + }, + ); + })->get(); + + expect(count($locations))->toEqual(6); +}); + + +test('whereMorphDoesntHaveRelation', function () { + $locations = Location::whereMorphDoesntHaveRelation( + 'capturable', + Character::class, + 'id', + 'TheonGreyjoy', + )->get(); + + expect(count($locations))->toEqual(5); +}); + +test('orWhereMorphDoesntHaveRelation', function () { + $locations = Location::where(function (Builder $query) { + $query->whereMorphDoesntHaveRelation( + 'capturable', + Character::class, + 'id', + 'DaenerysTargaryen', + ) + ->orWhereMorphDoesntHaveRelation( + 'capturable', + Character::class, + 'age', + '<', + 20, + ); + })->get(); + + expect(count($locations))->toEqual(1); +}); diff --git a/tests/Eloquent/RelationshipQueriesTest.php b/tests/Eloquent/RelationshipQueriesTest.php index 0740782..5f43160 100644 --- a/tests/Eloquent/RelationshipQueriesTest.php +++ b/tests/Eloquent/RelationshipQueriesTest.php @@ -1,7 +1,10 @@ toEqual(1); }); +test('has morph', function () { + $characters = Character::has('tags')->get(); + + expect(count($characters))->toEqual(2); +}); + + +test('orHas', function () { + $characters = Character::has('leads') + ->orHas('captured') + ->get(); + + expect(count($characters))->toEqual(4); +}); + test('doesntHave', function () { $characters = Character::doesntHave('leads')->get(); + expect(count($characters))->toEqual(40); }); -test('has on morphed relation', function () { - $characters = Character::has('tags')->get(); +test('orDoesntHave', function () { + $characters = Character::where(function (Builder $query) { + $query->doesntHave('leads') + ->orDoesntHave('conquered'); + }) + ->get(); - expect(count($characters))->toEqual(2); + $daenarys = $characters->first(function (Character $character) { + return $character->id === 'DaenerysTargaryen'; + }); + + expect(count($characters))->toEqual(42); + expect($daenarys)->toBeNull(); +}); + +test('whereHas', function () { + $locations = Location::whereHas('leader', function (Builder $query) { + $query->where('age', '<', 30); + }) + ->distinct() + ->pluck('led_by'); + + expect($locations->count())->toBe(2); + expect($locations[0])->toBe('DaenerysTargaryen'); + expect($locations[1])->toBe('SansaStark'); +}); + +test('orWhereHas', function () { + $locations = Location::where(function (Builder $query) { + $query->whereHas('leader', function (Builder $query) { + $query->where('age', '<', 15); + })->orWhereHas('leader', function (Builder $query) { + $query->where('age', '>', 30); + }); + }) + ->distinct() + ->pluck('led_by'); + + expect($locations->count())->toBe(2); + expect($locations[0])->toBe('CerseiLannister'); + expect($locations[1])->toBe('SansaStark'); +}); + +test('whereDoesntHave', function () { + $characters = Character::whereDoesntHave('leads', function (Builder $query) { + $query->where('name', 'Astapor'); + })->get(); + + $daenarys = $characters->first(function (Character $character) { + return $character->id === 'DaenerysTargaryen'; + }); + expect($characters->count())->toBe(42); + expect($daenarys)->toBeNull(); +}); + +test('orWhereDoesntHave', function () { + $houses = House::where(function (Builder $query) { + $query->whereDoesntHave('head', function (Builder $query) { + $query->where('age', '<', 20); + }) + ->orWhereDoesntHave('head', function (Builder $query) { + $query->whereNull('age'); + }); + })->get(); + + expect($houses[0]->name)->toBe('Stark'); + expect($houses[1]->name)->toBe('Targaryen'); +}); + + + +test('whereRelation', function () { + $locations = Location::whereRelation('leader', 'age', '<', 30) + ->distinct() + ->pluck('led_by'); + + expect($locations->count())->toBe(2); + expect($locations[0])->toBe('DaenerysTargaryen'); + expect($locations[1])->toBe('SansaStark'); +}); + +test('orWhereRelation', function () { + $locations = Location::where(function (Builder $query) { + $query->whereRelation('leader', 'age', '<', 15) + ->orWhereRelation('leader', 'age', '>', 30); + }) + ->distinct() + ->pluck('led_by'); + + expect($locations->count())->toBe(2); + expect($locations[0])->toBe('CerseiLannister'); + expect($locations[1])->toBe('SansaStark'); +}); + +test('whereDoesntHaveRelation', function () { + $characters = Character::whereDoesntHaveRelation('leads', 'name', 'Astapor')->get(); + + $daenarys = $characters->first(function (Character $character) { + return $character->id === 'DaenerysTargaryen'; + }); + + expect($characters->count())->toBe(42); + expect($daenarys)->toBeNull(); +}); + +test('orWhereDoesntHaveRelation', function () { + $houses = House::where(function (Builder $query) { + $query->whereDoesntHaveRelation('head', 'age', '<', 20) + ->orWhereDoesntHaveRelation('head', 'age', null); + })->get(); + + expect($houses[0]->name)->toBe('Stark'); + expect($houses[1]->name)->toBe('Targaryen'); }); test('withCount', function () { From 834de2837d2a434279654b2e613ba86c92fd131c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Jan 2025 03:27:06 +0000 Subject: [PATCH 32/34] chore(deps): bump dependabot/fetch-metadata from 2.2.0 to 2.3.0 Bumps [dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata) from 2.2.0 to 2.3.0. - [Release notes](https://github.com/dependabot/fetch-metadata/releases) - [Commits](https://github.com/dependabot/fetch-metadata/compare/v2.2.0...v2.3.0) --- updated-dependencies: - dependency-name: dependabot/fetch-metadata dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/dependabot-auto-merge.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index eb537d8..2cb1d5e 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -13,7 +13,7 @@ jobs: - name: Dependabot metadata id: metadata - uses: dependabot/fetch-metadata@v2.2.0 + uses: dependabot/fetch-metadata@v2.3.0 with: github-token: "${{ secrets.GITHUB_TOKEN }}" From 045758378ff539907f24b5ee2ac378454b8c7192 Mon Sep 17 00:00:00 2001 From: Laravel Freelancer NL <36150929+LaravelFreelancerNL@users.noreply.github.com> Date: Wed, 12 Mar 2025 16:53:34 +0100 Subject: [PATCH 33/34] Adds support for Laravel 12 at the expense of Laravel 11 (#192) * Set minimum version to 11, major only * Added Laravel 12 to the matrix * updated deps * updated php * Updated method call compatibility * Fixed phpmd suppressions to be compatible with the latest phpstan. * Parent compatibility fix * Grammar creation compatibility fix * Made tests more resilient against changes in the amount of system analyzers * Made tests more resilient against changes in the amount of system analyzers * Test against ray usage * Set connection the laravel 12 way * Check for analyzer count regardless of changes in system analyzers * Style fix * Ignored rules and files that don't apply * phpstan checks * ci: removed Laravel 11 from workflows * qa: Updated phpdocs * feat!: dropped laravel 11 due to incompatibilities. * docs: updated supported versions --------- Co-authored-by: Deploy --- .github/workflows/coverage.yml | 6 +- .github/workflows/run-tests.yml | 6 +- composer.json | 13 ++--- phpstan.neon | 5 +- phpstan.neon.dist | 5 +- readme.md | 9 +-- src/AranguentServiceProvider.php | 2 +- src/Concerns/RunsQueries.php | 6 +- src/Connection.php | 6 +- src/Console/DbCommand.php | 2 +- src/Console/Migrations/MigrateMakeCommand.php | 2 +- src/Console/TableCommand.php | 13 ++++- src/Eloquent/Casts/AsArrayObject.php | 4 +- src/Eloquent/Casts/AsCollection.php | 8 +-- src/Eloquent/Casts/AsEnumArrayObject.php | 16 ++--- src/Eloquent/Casts/AsEnumCollection.php | 6 +- .../Concerns/HasAranguentRelationships.php | 4 +- src/Eloquent/Concerns/HasAttributes.php | 2 +- .../QueriesAranguentRelationships.php | 5 +- src/Migrations/MigrationCreator.php | 4 +- src/Providers/CommandServiceProvider.php | 2 +- src/Query/Builder.php | 3 +- src/Query/Concerns/BuildsGroups.php | 4 +- src/Query/Concerns/BuildsJoins.php | 10 ++-- src/Query/Concerns/BuildsSelects.php | 2 +- src/Query/Concerns/BuildsSubqueries.php | 2 +- src/Query/Concerns/BuildsWheres.php | 23 ++++---- src/Query/Concerns/CompilesAggregates.php | 2 +- src/Query/Concerns/CompilesColumns.php | 2 +- src/Query/Concerns/CompilesFilters.php | 10 ++-- src/Query/Concerns/HandlesAliases.php | 2 +- src/Query/Concerns/HandlesAqlGrammar.php | 6 +- src/Query/Grammar.php | 8 +-- src/Query/Processor.php | 2 +- src/Schema/Builder.php | 14 ++--- src/Schema/Concerns/HandlesGraphs.php | 2 +- src/Schema/Concerns/HandlesViews.php | 5 +- src/Schema/Grammar.php | 14 ++++- .../Concerns/InteractsWithDatabase.php | 2 +- tests/ArchTest.php | 12 ++-- tests/Console/MigrateFreshCommandTest.php | 28 ++++++--- tests/Console/WipeCommandTest.php | 24 +++++--- tests/Pest.php | 4 +- tests/Query/GrammarTest.php | 4 +- tests/Query/IdKeyConversionTest.php | 2 +- tests/Query/InsertTest.php | 2 +- tests/Query/OrderingTest.php | 6 +- tests/Query/WheresTest.php | 58 +++++++++---------- tests/Schema/AnalyzerTest.php | 4 +- tests/Schema/ColumnTest.php | 2 +- tests/Schema/SchemaBuilderTest.php | 3 +- 51 files changed, 219 insertions(+), 169 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 83d2599..da8b5e4 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -10,10 +10,10 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - laravel: ['^11.0'] + laravel: ['^12.0'] include: - - laravel: '^11.0' - testbench: '^9.0' + - laravel: '^12.0' + testbench: '^10.0' name: Test coverage (Scrutinizer) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 1840ea4..a17c471 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -17,10 +17,10 @@ jobs: matrix: arangodb: ['3.11', '3.12'] php: ['8.2', '8.3', '8.4'] - laravel: ['^11.0'] + laravel: ['^12.0'] include: - - laravel: '^11.0' - testbench: '^9.0' + - laravel: '^12.0' + testbench: '^10.0' name: QA L ${{ matrix.laravel }} / P ${{ matrix.php }} / A ${{ matrix.arangodb }} - ${{ matrix.dependency-version }} diff --git a/composer.json b/composer.json index 43b3f49..24586d6 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ "composer/composer": "^2.8.0", "laravel-freelancer-nl/arangodb-php-client": "^2.8.0", "laravel-freelancer-nl/fluentaql": "^2.0", - "laravel/framework": "^11.0", + "laravel/framework": "^12.0", "spatie/laravel-data": "^4.4.0", "stevebauman/unfinalize": "^2.1", "vlucas/phpdotenv": "^5.4" @@ -36,13 +36,12 @@ "laravel/pint": "^1.10", "mockery/mockery": "^1.5.0", "nunomaduro/collision": "^8.0", - "larastan/larastan": "^2.0", - "orchestra/testbench": "^9.0", - "pestphp/pest": "^2.6.1", - "pestphp/pest-plugin-faker": "^2.0", - "pestphp/pest-plugin-laravel": "^2.0", + "larastan/larastan": "^3.1", + "orchestra/testbench": "^10.0", + "pestphp/pest": "^3.7.4", + "pestphp/pest-plugin-faker": "^3.0", + "pestphp/pest-plugin-laravel": "^3.1", "phpmd/phpmd": "2.13", - "phpstan/phpstan": "^1.0", "spatie/laravel-ray": "^1.32", "timacdonald/log-fake": "^2.2.0" }, diff --git a/phpstan.neon b/phpstan.neon index a00e0c8..0e502c6 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -5,9 +5,12 @@ parameters: level: 8 ignoreErrors: - identifier: missingType.generics + - identifier: trait.unused universalObjectCratesClasses: - 'Illuminate\Support\Fluent' paths: - src excludePaths: - - src/Schema \ No newline at end of file + - src/Schema + - src/Facades/Schema.php + treatPhpDocTypesAsCertain: false \ No newline at end of file diff --git a/phpstan.neon.dist b/phpstan.neon.dist index a00e0c8..0e502c6 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -5,9 +5,12 @@ parameters: level: 8 ignoreErrors: - identifier: missingType.generics + - identifier: trait.unused universalObjectCratesClasses: - 'Illuminate\Support\Fluent' paths: - src excludePaths: - - src/Schema \ No newline at end of file + - src/Schema + - src/Facades/Schema.php + treatPhpDocTypesAsCertain: false \ No newline at end of file diff --git a/readme.md b/readme.md index 8af3f21..2e9ba45 100644 --- a/readme.md +++ b/readme.md @@ -34,10 +34,11 @@ You may then use composer to install Aranguent: ### Version compatibility -| Laravel | ArangoDB | PHP | Aranguent | -|:--------------|:---------|:-----|:----------| -| ^8.0 and ^9.0 | ^3.7 | ^8.0 | ^0.13 | -| ^11.0 | ^3.11 | ^8.2 | ^1.0.0 | +| Laravel | ArangoDB | PHP | Aranguent | +|:--------------|:---------|:-----|:-------------------------| +| ^8.0 and ^9.0 | ^3.7 | ^8.0 | ^0.13 | +| ^11.0 | ^3.11 | ^8.2 | ^1.0.0 - 1.0.0-beta.11 | +| ^12.0 | ^3.11 | ^8.2 | ^v1.0.0-beta.12 | ## Documentation 1) [Connect to ArangoDB](docs/connect-to-arangodb.md): set up a connection diff --git a/src/AranguentServiceProvider.php b/src/AranguentServiceProvider.php index 14128d2..f450b9f 100644 --- a/src/AranguentServiceProvider.php +++ b/src/AranguentServiceProvider.php @@ -72,7 +72,7 @@ function ($db) { function ($config, $name) { $config['name'] = $name; $connection = new Connection($config); - $connection->setSchemaGrammar(new SchemaGrammar()); + $connection->setSchemaGrammar(new SchemaGrammar($connection)); return $connection; }, diff --git a/src/Concerns/RunsQueries.php b/src/Concerns/RunsQueries.php index bc4c679..28cae0e 100644 --- a/src/Concerns/RunsQueries.php +++ b/src/Concerns/RunsQueries.php @@ -33,7 +33,7 @@ protected function isUniqueConstraintError(Exception $exception) * @param array $bindings * @param bool $useReadPdo * @return \Generator - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") */ public function cursor($query, $bindings = [], $useReadPdo = true) { @@ -195,7 +195,7 @@ protected function handleQueryBuilder($query, array $bindings): array /** * Run a select statement against the database. * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") * * @param string|FluentAqlBuilder $query * @param array $bindings @@ -210,7 +210,7 @@ public function select($query, $bindings = [], $useReadPdo = true) /** * Run an AQL query against the database and return the results. * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") * * @param string|FluentAqlBuilder $query * @param array $bindings diff --git a/src/Connection.php b/src/Connection.php index b60a5c9..3ca2992 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -93,7 +93,7 @@ protected function getDefaultPostProcessor(): Processor */ protected function getDefaultQueryGrammar() { - ($grammar = new QueryGrammar())->setConnection($this); + $grammar = new QueryGrammar($this); return $grammar; } @@ -201,7 +201,7 @@ protected function escapeBinary($value) * @param bool $binary * @return string * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") */ public function escape($value, $binary = false) { @@ -221,7 +221,7 @@ public function escape($value, $binary = false) * @param string $value * @return string * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") */ protected function escapeString($value, bool $binary = false) { diff --git a/src/Console/DbCommand.php b/src/Console/DbCommand.php index 465fdf4..b0dfda2 100644 --- a/src/Console/DbCommand.php +++ b/src/Console/DbCommand.php @@ -14,7 +14,7 @@ class DbCommand extends IlluminateDbCommand * * @return int * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings("PHPMD.UnusedFormalParameter") */ public function handle() { diff --git a/src/Console/Migrations/MigrateMakeCommand.php b/src/Console/Migrations/MigrateMakeCommand.php index 3fae1ee..d782055 100644 --- a/src/Console/Migrations/MigrateMakeCommand.php +++ b/src/Console/Migrations/MigrateMakeCommand.php @@ -111,7 +111,7 @@ public function handle() * * @throws Exception * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") */ protected function writeMigration($name, $table, $create, $edge = false) { diff --git a/src/Console/TableCommand.php b/src/Console/TableCommand.php index 5302e93..ff620ff 100644 --- a/src/Console/TableCommand.php +++ b/src/Console/TableCommand.php @@ -66,10 +66,17 @@ public function handle(ConnectionResolverInterface $connections) return 1; } - $tableName = $this->withoutTablePrefix($connection, $table['name']); + [$columns, $indexes] = $connection->withoutTablePrefix(function ($connection) use ($table) { + $schema = $connection->getSchemaBuilder(); + $tableName = $table['name']; + + return [ + $this->columns($schema, $tableName), + $this->indexes($schema, $tableName), + ]; + }); + - $columns = $this->columns($schema, $tableName); - $indexes = $this->indexes($schema, $tableName); $data = [ 'table' => $table, diff --git a/src/Eloquent/Casts/AsArrayObject.php b/src/Eloquent/Casts/AsArrayObject.php index 7cb56fe..b57203c 100644 --- a/src/Eloquent/Casts/AsArrayObject.php +++ b/src/Eloquent/Casts/AsArrayObject.php @@ -17,7 +17,7 @@ class AsArrayObject extends IlluminateAsArrayObject * @param array $arguments * @return \Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Database\Eloquent\Casts\ArrayObject, iterable> * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings("PHPMD.UnusedFormalParameter") */ public static function castUsing(array $arguments) { @@ -44,7 +44,7 @@ public function get($model, $key, $value, $attributes) * @param mixed[] $attributes * @return mixed[] * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings("PHPMD.UnusedFormalParameter") */ public function set($model, $key, $value, $attributes) { diff --git a/src/Eloquent/Casts/AsCollection.php b/src/Eloquent/Casts/AsCollection.php index e10056e..3507e08 100644 --- a/src/Eloquent/Casts/AsCollection.php +++ b/src/Eloquent/Casts/AsCollection.php @@ -11,8 +11,8 @@ use LaravelFreelancerNL\Aranguent\Eloquent\Model; /** - * @SuppressWarnings(PHPMD.UndefinedVariable) - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings("PHPMD.UndefinedVariable") + * @SuppressWarnings("PHPMD.UnusedFormalParameter") */ class AsCollection extends IlluminateAsCollection { @@ -37,7 +37,7 @@ public function __construct(protected array $arguments) {} * @param $attributes * @return Collection|mixed|void|null * - * @SuppressWarnings(PHPMD.UndefinedVariable) + * @SuppressWarnings("PHPMD.UndefinedVariable") */ public function get($model, $key, $value, $attributes) { @@ -68,7 +68,7 @@ public function get($model, $key, $value, $attributes) * @param mixed[] $attributes * @return mixed[] * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings("PHPMD.UnusedFormalParameter") */ public function set($model, $key, $value, $attributes) { diff --git a/src/Eloquent/Casts/AsEnumArrayObject.php b/src/Eloquent/Casts/AsEnumArrayObject.php index 54f4915..dc6df72 100644 --- a/src/Eloquent/Casts/AsEnumArrayObject.php +++ b/src/Eloquent/Casts/AsEnumArrayObject.php @@ -10,8 +10,8 @@ use LaravelFreelancerNL\Aranguent\Eloquent\Model; /** - * @SuppressWarnings(PHPMD.UndefinedVariable) - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings("PHPMD.UndefinedVariable") + * @SuppressWarnings("PHPMD.UnusedFormalParameter") */ class AsEnumArrayObject extends IlluminateAsEnumArrayObjectAlias { @@ -23,7 +23,7 @@ class AsEnumArrayObject extends IlluminateAsEnumArrayObjectAlias * @param array{class-string} $arguments * @return \Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Database\Eloquent\Casts\ArrayObject, iterable> * - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @SuppressWarnings("PHPMD.ExcessiveMethodLength") */ public static function castUsing(array $arguments) { @@ -36,7 +36,7 @@ public static function castUsing(array $arguments) /** * @param array> $arguments * - * @SuppressWarnings(PHPMD.UndefinedVariable) + * @SuppressWarnings("PHPMD.UndefinedVariable") */ public function __construct(array $arguments) { @@ -50,7 +50,7 @@ public function __construct(array $arguments) * @param $attributes * @return ArrayObject|void * - * @SuppressWarnings(PHPMD.UndefinedVariable) + * @SuppressWarnings("PHPMD.UndefinedVariable") */ public function get($model, $key, $value, $attributes) { @@ -83,8 +83,8 @@ public function get($model, $key, $value, $attributes) * @param mixed[] $attributes * @return mixed[] * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * @SuppressWarnings(PHPMD.UndefinedVariable) + * @SuppressWarnings("PHPMD.UnusedFormalParameter") + * @SuppressWarnings("PHPMD.UndefinedVariable") */ public function set($model, $key, $value, $attributes) { @@ -136,7 +136,7 @@ protected function getStorableEnumValue($enum) * @param class-string $class * @return string * - * @SuppressWarnings(PHPMD.ShortMethodName) + * @SuppressWarnings("PHPMD.ShortMethodName") */ public static function of($class) { diff --git a/src/Eloquent/Casts/AsEnumCollection.php b/src/Eloquent/Casts/AsEnumCollection.php index 334323a..37bbeef 100644 --- a/src/Eloquent/Casts/AsEnumCollection.php +++ b/src/Eloquent/Casts/AsEnumCollection.php @@ -11,9 +11,9 @@ use LaravelFreelancerNL\Aranguent\Eloquent\Model; /** - * @SuppressWarnings(PHPMD.UndefinedVariable) - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * @SuppressWarnings(PHPMD.ShortMethodName) + * @SuppressWarnings("PHPMD.UndefinedVariable") + * @SuppressWarnings("PHPMD.UnusedFormalParameter") + * @SuppressWarnings("PHPMD.ShortMethodName") */ class AsEnumCollection extends IlluminateAsEnumCollection { diff --git a/src/Eloquent/Concerns/HasAranguentRelationships.php b/src/Eloquent/Concerns/HasAranguentRelationships.php index 3289b8a..b956e08 100644 --- a/src/Eloquent/Concerns/HasAranguentRelationships.php +++ b/src/Eloquent/Concerns/HasAranguentRelationships.php @@ -163,8 +163,8 @@ protected function newMorphTo(Builder $query, Model $parent, $foreignKey, $owner * * Laravel API PHPMD exclusions * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) - * @SuppressWarnings(PHPMD.ExcessiveParameterList) + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") + * @SuppressWarnings("PHPMD.ExcessiveParameterList") * * @param string $name * @param string $table diff --git a/src/Eloquent/Concerns/HasAttributes.php b/src/Eloquent/Concerns/HasAttributes.php index 37feade..7ed02e4 100644 --- a/src/Eloquent/Concerns/HasAttributes.php +++ b/src/Eloquent/Concerns/HasAttributes.php @@ -24,7 +24,7 @@ protected function isJsonCastable($key) * @param bool $asObject * @return mixed * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") */ public function fromJson($value, $asObject = false) { diff --git a/src/Eloquent/Concerns/QueriesAranguentRelationships.php b/src/Eloquent/Concerns/QueriesAranguentRelationships.php index fa16bc8..43ed9b0 100644 --- a/src/Eloquent/Concerns/QueriesAranguentRelationships.php +++ b/src/Eloquent/Concerns/QueriesAranguentRelationships.php @@ -125,7 +125,7 @@ public function mergeConstraintsFrom(Builder $from) * @param string $function * @return $this * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings("PHPMD.CyclomaticComplexity") */ public function withAggregate($relations, $column, $function = null) { @@ -179,7 +179,8 @@ public function withAggregate($relations, $column, $function = null) // If the query contains certain elements like orderings / more than one column selected // then we will remove those elements from the query so that it will execute properly // when given to the database. Otherwise, we may receive SQL errors or poor syntax. - unset($query->orders); + $query->orders = null; + $query->setBindings([], 'order'); if (is_array($query->columns) && count($query->columns) > 1) { diff --git a/src/Migrations/MigrationCreator.php b/src/Migrations/MigrationCreator.php index c786c57..f560b33 100644 --- a/src/Migrations/MigrationCreator.php +++ b/src/Migrations/MigrationCreator.php @@ -59,7 +59,7 @@ public function stubPath() * * @throws FileNotFoundException * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") */ protected function getStub($table, $create, $edge = false) { @@ -107,7 +107,7 @@ protected function getStub($table, $create, $edge = false) * * @throws \Exception */ - /** @phpstan-ignore-next-line @SuppressWarnings(PHPMD.BooleanArgumentFlag) */ + /** @phpstan-ignore-next-line @SuppressWarnings("PHPMD.BooleanArgumentFlag") */ public function create($name, $path, $table = null, $create = false, $edge = false) { if ($this->useFallback()) { diff --git a/src/Providers/CommandServiceProvider.php b/src/Providers/CommandServiceProvider.php index f2460bb..cfacb90 100644 --- a/src/Providers/CommandServiceProvider.php +++ b/src/Providers/CommandServiceProvider.php @@ -48,7 +48,7 @@ public function register() * @param string[] $commands * @return void * - * @SuppressWarnings(PHPMD.ElseExpression) + * @SuppressWarnings("PHPMD.ElseExpression") */ protected function registerCommands(array $commands) { diff --git a/src/Query/Builder.php b/src/Query/Builder.php index 0d42f88..e47355e 100644 --- a/src/Query/Builder.php +++ b/src/Query/Builder.php @@ -115,12 +115,11 @@ class Builder extends IlluminateQueryBuilder * Create a new query builder instance. */ public function __construct( - IlluminateConnectionInterface $connection, + IlluminateConnectionInterface $connection, ?IlluminateQueryGrammar $grammar = null, ?IlluminateProcessor $processor = null, ?AQB $aqb = null, ) { - assert($connection instanceof IlluminateConnectionInterface); assert($processor instanceof IlluminateProcessor); parent::__construct($connection, $grammar, $processor); diff --git a/src/Query/Concerns/BuildsGroups.php b/src/Query/Concerns/BuildsGroups.php index 7607ec3..b49fbb5 100644 --- a/src/Query/Concerns/BuildsGroups.php +++ b/src/Query/Concerns/BuildsGroups.php @@ -56,6 +56,8 @@ public function groupByRaw($aql, array $bindings = []) public function cleanGroupVariables(): void { + // FIXME: check for possible expressions instead of strings. + /* @phpstan-ignore-next-line */ $this->tableAliases = array_diff($this->tableAliases, $this->groupVariables ?? []); $this->groupVariables = null; } @@ -195,7 +197,7 @@ public function addNestedHavingQuery($query, $boolean = 'and') * @param bool $not * @return $this * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") */ public function havingBetween($column, iterable $values, $boolean = 'and', $not = false) { diff --git a/src/Query/Concerns/BuildsJoins.php b/src/Query/Concerns/BuildsJoins.php index d8cbe67..caf419d 100644 --- a/src/Query/Concerns/BuildsJoins.php +++ b/src/Query/Concerns/BuildsJoins.php @@ -23,7 +23,7 @@ trait BuildsJoins * @param \Illuminate\Contracts\Database\Query\Expression|string|null $second * @return $this * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings("PHPMD.UnusedFormalParameter") */ public function rightJoin($table, $first, $operator = null, $second = null) { @@ -40,7 +40,7 @@ public function rightJoin($table, $first, $operator = null, $second = null) * @param \Illuminate\Contracts\Database\Query\Expression|string|null $second * @return $this * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings("PHPMD.UnusedFormalParameter") */ public function rightJoinSub($query, $as, $first, $operator = null, $second = null) { @@ -56,7 +56,7 @@ public function rightJoinSub($query, $as, $first, $operator = null, $second = nu * @param \Illuminate\Contracts\Database\Query\Expression|string $second * @return $this * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings("PHPMD.UnusedFormalParameter") */ public function rightJoinWhere($table, $first, $operator, $second) { @@ -69,7 +69,7 @@ public function rightJoinWhere($table, $first, $operator, $second) * * The boolean argument flag is part of this method's API in Laravel. * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") * * @param mixed $table * @param Closure|string $first @@ -117,7 +117,7 @@ public function join($table, $first, $operator = null, $second = null, $type = ' * * @throws \InvalidArgumentException * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") */ public function joinSub($query, $as, $first, $operator = null, $second = null, $type = 'inner', $where = false): IlluminateQueryBuilder { diff --git a/src/Query/Concerns/BuildsSelects.php b/src/Query/Concerns/BuildsSelects.php index 3360f43..ba99513 100644 --- a/src/Query/Concerns/BuildsSelects.php +++ b/src/Query/Concerns/BuildsSelects.php @@ -229,7 +229,7 @@ public function inRandomOrder($seed = '') * @param bool $all * @return $this * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") */ public function union($query, $all = false) { diff --git a/src/Query/Concerns/BuildsSubqueries.php b/src/Query/Concerns/BuildsSubqueries.php index 22cba48..02d5475 100644 --- a/src/Query/Concerns/BuildsSubqueries.php +++ b/src/Query/Concerns/BuildsSubqueries.php @@ -28,7 +28,7 @@ trait BuildsSubqueries * @param \Closure|IlluminateQueryBuilder|IlluminateEloquentBuilder|string $query * @return array * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") */ public function createSub($query, bool $returnSingleValue = false) { diff --git a/src/Query/Concerns/BuildsWheres.php b/src/Query/Concerns/BuildsWheres.php index e4ddf55..7b05155 100644 --- a/src/Query/Concerns/BuildsWheres.php +++ b/src/Query/Concerns/BuildsWheres.php @@ -6,6 +6,7 @@ use Carbon\CarbonPeriod; use Closure; +use DateTimeInterface; use Illuminate\Contracts\Database\Query\ConditionExpression; use Illuminate\Contracts\Support\Arrayable; use Illuminate\Database\Eloquent\Builder as IlluminateEloquentBuilder; @@ -27,7 +28,7 @@ trait BuildsWheres * @param string $boolean * @return $this * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings("PHPMD.UnusedFormalParameter") */ public function whereFullText($columns, $value, array $options = [], $boolean = 'and') { @@ -40,14 +41,14 @@ public function whereFullText($columns, $value, array $options = [], $boolean = /** * Prepare the value and operator for a where clause. * - * @param float|int|string|null $value + * @param DateTimeInterface|float|int|string|null $value * @param string|null $operator * @param bool $useDefault * @return array * * @throws \InvalidArgumentException * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") */ public function prepareValueAndOperator($value, $operator, $useDefault = false) { @@ -157,7 +158,7 @@ public function addNestedWhereQuery($query, $boolean = 'and') * @param bool $not * @return $this * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") */ public function addWhereExistsQuery(IlluminateQueryBuilder $query, $boolean = 'and', $not = false) { @@ -195,8 +196,8 @@ public function mergeWheres($wheres, $bindings) * @param string $boolean * @return IlluminateQueryBuilder * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings("PHPMD.CyclomaticComplexity") + * @SuppressWarnings("PHPMD.NPathComplexity") */ public function where($column, $operator = null, $value = null, $boolean = 'and') { @@ -286,7 +287,7 @@ public function where($column, $operator = null, $value = null, $boolean = 'and' * @param bool $not * @return IlluminateQueryBuilder * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") */ public function whereBetween($column, iterable $values, $boolean = 'and', $not = false) { @@ -356,7 +357,7 @@ public function whereColumn($first, $operator = null, $second = null, $boolean = * @param bool $not * @return IlluminateQueryBuilder * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") */ public function whereIn($column, $values, $boolean = 'and', $not = false) { @@ -390,7 +391,7 @@ public function whereIn($column, $values, $boolean = 'and', $not = false) /** * Add a "where JSON contains" clause to the query. * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") * * @param string $column * @param mixed $value @@ -445,7 +446,7 @@ public function whereJsonLength($column, $operator, $value = null, $boolean = 'a * @param bool $not * @return $this * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") */ public function whereLike($column, $value, $caseSensitive = false, $boolean = 'and', $not = false) { @@ -466,7 +467,7 @@ public function whereLike($column, $value, $caseSensitive = false, $boolean = 'a * @param bool $not * @return $this * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") */ public function whereNull($columns, $boolean = 'and', $not = false) { diff --git a/src/Query/Concerns/CompilesAggregates.php b/src/Query/Concerns/CompilesAggregates.php index 27ecca6..be3edc2 100644 --- a/src/Query/Concerns/CompilesAggregates.php +++ b/src/Query/Concerns/CompilesAggregates.php @@ -40,7 +40,7 @@ protected function compileAvg(Builder $query, array $aggregate) /** * Compile AQL for count aggregate. * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings("PHPMD.UnusedFormalParameter") * @param Builder $query * @return string */ diff --git a/src/Query/Concerns/CompilesColumns.php b/src/Query/Concerns/CompilesColumns.php index 50c3966..fb0bca1 100644 --- a/src/Query/Concerns/CompilesColumns.php +++ b/src/Query/Concerns/CompilesColumns.php @@ -76,7 +76,7 @@ protected function compileColumns(IlluminateQueryBuilder $query, $columns) * @return array * @throws Exception * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings("PHPMD.CyclomaticComplexity") */ protected function prepareColumns(IlluminateQueryBuilder $query, array $columns) { diff --git a/src/Query/Concerns/CompilesFilters.php b/src/Query/Concerns/CompilesFilters.php index 291ed47..047d3ce 100644 --- a/src/Query/Concerns/CompilesFilters.php +++ b/src/Query/Concerns/CompilesFilters.php @@ -42,7 +42,7 @@ protected function compileWheresToArray($query) * @param array $aql * @return string * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings("PHPMD.UnusedFormalParameter") */ protected function concatenateWhereClauses($query, $aql) { @@ -357,7 +357,7 @@ protected function filterJsonLength(IlluminateQueryBuilder $query, array $filter * @param array $filter * @return mixed * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings("PHPMD.UnusedFormalParameter") */ protected function filterExpression(IlluminateQueryBuilder $query, $filter) { @@ -526,7 +526,7 @@ protected function filterSub(IlluminateQueryBuilder $query, $filter) * @param array $filter * @return string * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings("PHPMD.UnusedFormalParameter") */ protected function filterExists(IlluminateQueryBuilder $query, $filter) { @@ -540,7 +540,7 @@ protected function filterExists(IlluminateQueryBuilder $query, $filter) * @param array $filter * @return string * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings("PHPMD.UnusedFormalParameter") */ protected function filterNotExists(IlluminateQueryBuilder $query, $filter) { @@ -554,7 +554,7 @@ protected function filterNotExists(IlluminateQueryBuilder $query, $filter) * @param array $filter * @return string * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings("PHPMD.UnusedFormalParameter") */ protected function filterNested(IlluminateQueryBuilder $query, $filter) { diff --git a/src/Query/Concerns/HandlesAliases.php b/src/Query/Concerns/HandlesAliases.php index 784a57c..0d081c2 100644 --- a/src/Query/Concerns/HandlesAliases.php +++ b/src/Query/Concerns/HandlesAliases.php @@ -175,7 +175,7 @@ public function registerColumnAlias(string $column, ?string $alias = null): bool } /** - * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings("PHPMD.CyclomaticComplexity") */ public function registerTableAlias(string|Expression $table, ?string $alias = null): string { diff --git a/src/Query/Concerns/HandlesAqlGrammar.php b/src/Query/Concerns/HandlesAqlGrammar.php index cfc0794..07ebf37 100644 --- a/src/Query/Concerns/HandlesAqlGrammar.php +++ b/src/Query/Concerns/HandlesAqlGrammar.php @@ -116,7 +116,7 @@ public function quoteString($value) * @param Array|Expression|string $value * @return array|float|int|string * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") */ public function wrap($value) { @@ -148,10 +148,10 @@ public function wrap($value) * @param Expression|string $table * @return float|int|string */ - public function wrapTable($table) + public function wrapTable($table, $prefix = null) { if (!$table instanceof Expression) { - $wrappedTable = $this->wrap($this->tablePrefix . $table); + $wrappedTable = $this->wrap(($prefix ?? $this->tablePrefix) . $table); assert(!is_array($wrappedTable)); diff --git a/src/Query/Grammar.php b/src/Query/Grammar.php index 903fdd4..54dc477 100644 --- a/src/Query/Grammar.php +++ b/src/Query/Grammar.php @@ -264,7 +264,7 @@ protected function compileFrom(IlluminateQueryBuilder $query, $table) * * @param array $options * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings("PHPMD.UnusedFormalParameter") */ protected function compileFromOptions($options): string { @@ -297,7 +297,7 @@ protected function compilePostIterationVariables(IlluminateQueryBuilder $query, * @param array $variables * @return string * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings("PHPMD.UnusedFormalParameter") */ protected function compileVariables(IlluminateQueryBuilder $query, array $variables): string { @@ -367,7 +367,7 @@ protected function compileOrdersToArray(IlluminateQueryBuilder $query, $orders, * @param int $offset * @return string * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings("PHPMD.UnusedFormalParameter") */ protected function compileOffset(IlluminateQueryBuilder $query, $offset) { @@ -383,7 +383,7 @@ protected function compileOffset(IlluminateQueryBuilder $query, $offset) * @param int $limit * @return string * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings("PHPMD.UnusedFormalParameter") */ protected function compileLimit(IlluminateQueryBuilder $query, $limit) { diff --git a/src/Query/Processor.php b/src/Query/Processor.php index c3f1d23..7764e4e 100644 --- a/src/Query/Processor.php +++ b/src/Query/Processor.php @@ -12,7 +12,7 @@ class Processor extends IlluminateProcessor /** * Process the results of a "select" query. * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings("PHPMD.UnusedFormalParameter") * * @param array|null $results * @return array diff --git a/src/Schema/Builder.php b/src/Schema/Builder.php index f8bd902..6e1be8f 100644 --- a/src/Schema/Builder.php +++ b/src/Schema/Builder.php @@ -129,11 +129,14 @@ public function getAllTables(): array /** * Get the tables that belong to the database. * + * @param string|string[]|null $schema * @return array * @throws ArangoException */ - public function getTables() + public function getTables($schema = null) { + unset($schema); + return $this->mapResultsToArray( $this->schemaManager->getCollections(true), ); @@ -197,14 +200,7 @@ public function hasColumn($table, $column) */ public function getColumns($table) { - $parameters = []; - $parameters['name'] = 'columns'; - $parameters['handler'] = 'aql'; - $parameters['table'] = $table; - - $command = new Fluent($parameters); - - $compilation = $this->grammar->compileColumns($table, $command); + $compilation = $this->grammar->compileColumns(null, $table); $rawColumns = $this->connection->select($compilation['aqb'], $compilation['bindings']); diff --git a/src/Schema/Concerns/HandlesGraphs.php b/src/Schema/Concerns/HandlesGraphs.php index db3ea48..7557155 100644 --- a/src/Schema/Concerns/HandlesGraphs.php +++ b/src/Schema/Concerns/HandlesGraphs.php @@ -12,7 +12,7 @@ trait HandlesGraphs * @param array $properties * @throws ArangoException * - * @SuppressWarnings(PHPMD.BooleanArgumentFlag) + * @SuppressWarnings("PHPMD.BooleanArgumentFlag") */ public function createGraph(string $name, array $properties = [], bool $waitForSync = false) { diff --git a/src/Schema/Concerns/HandlesViews.php b/src/Schema/Concerns/HandlesViews.php index a9a744f..a775a25 100644 --- a/src/Schema/Concerns/HandlesViews.php +++ b/src/Schema/Concerns/HandlesViews.php @@ -40,11 +40,14 @@ public function hasView($view) } /** + * @param string|string[]|null $schema * @return mixed[] * @throws ArangoException */ - public function getViews(): array + public function getViews($schema = null): array { + unset($schema); + return $this->mapResultsToArray( $this->schemaManager->getViews(), ); diff --git a/src/Schema/Grammar.php b/src/Schema/Grammar.php index 1f49b9c..7bbf97a 100644 --- a/src/Schema/Grammar.php +++ b/src/Schema/Grammar.php @@ -24,12 +24,24 @@ class Grammar extends IlluminateGrammar * Compile AQL to check if an attribute is in use within a document in the collection. * If multiple attributes are set then all must be set in one document. * + * @param string|null $schema * @param string $table * @return Fluent * @throws BindException */ - public function compileColumns($table, Fluent $command) + public function compileColumns($schema, $table) { + // At this time we don't use the schema; even if it has been set on the table. + unset($schema); + + $parameters = []; + $parameters['name'] = 'columns'; + $parameters['handler'] = 'aql'; + $parameters['table'] = $table; + + $command = new Fluent($parameters); + + $command->bindings = [ '@collection' => $table, ]; diff --git a/src/Testing/Concerns/InteractsWithDatabase.php b/src/Testing/Concerns/InteractsWithDatabase.php index c5e36af..a0f9195 100644 --- a/src/Testing/Concerns/InteractsWithDatabase.php +++ b/src/Testing/Concerns/InteractsWithDatabase.php @@ -17,7 +17,7 @@ trait InteractsWithDatabase * @param string|null $connection * @return \Illuminate\Contracts\Database\Query\Expression * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings("PHPMD.UnusedFormalParameter") */ public function castAsJson($value, $connection = null) { diff --git a/tests/ArchTest.php b/tests/ArchTest.php index b3b8763..2d41668 100644 --- a/tests/ArchTest.php +++ b/tests/ArchTest.php @@ -1,6 +1,10 @@ expect(['dd', 'dump', 'ray']) -// ->each->not->toBeUsed(); +declare (strict_types=1); + +/* + * Besides ray we don't test for debug function use as we need to test their proper functioning in the package. + */ +it('will not use debugging functions') + ->expect(['ray']) + ->each->not->toBeUsed(); diff --git a/tests/Console/MigrateFreshCommandTest.php b/tests/Console/MigrateFreshCommandTest.php index 25d4c2c..0e71f5a 100644 --- a/tests/Console/MigrateFreshCommandTest.php +++ b/tests/Console/MigrateFreshCommandTest.php @@ -84,6 +84,8 @@ }); test('migrate:fresh --drop-analyzers', function () { + $initialAnalyzers = $this->schemaManager->getAnalyzers(); + $path = [ realpath(__DIR__ . '/../../TestSetup/Database/Migrations'), ]; @@ -95,6 +97,9 @@ ); } + + $analyzersAfterCreation = $this->schemaManager->getAnalyzers(); + $this->artisan('migrate:fresh', [ '--path' => [ database_path('migrations'), @@ -108,8 +113,10 @@ ])->assertExitCode(0); - $analyzers = $this->schemaManager->getAnalyzers(); - expect(count($analyzers))->toBe(13); + $endAnalyzers = $this->schemaManager->getAnalyzers(); + + expect(count($analyzersAfterCreation))->toBe(1 + count($initialAnalyzers)); + expect(count($initialAnalyzers))->toBe(count($endAnalyzers)); }); test('migrate:fresh --drop-graphs', function () { @@ -151,6 +158,10 @@ }); test('migrate:fresh --drop-all', function () { + $initialViews = $this->schemaManager->getViews(); + $initialAnalyzers = $this->schemaManager->getAnalyzers(); + $initialGraphs = $this->schemaManager->getGraphs(); + $path = [ realpath(__DIR__ . '/../../TestSetup/Database/Migrations'), ]; @@ -194,15 +205,14 @@ ])->assertExitCode(0); + $endAnalyzers = $this->schemaManager->getAnalyzers(); + $endGraphs = $this->schemaManager->getGraphs(); + $endViews = $this->schemaManager->getViews(); - $analyzers = $this->schemaManager->getAnalyzers(); - expect(count($analyzers))->toBe(13); + expect(count($initialAnalyzers))->toBe(count($endAnalyzers)); + expect(count($initialGraphs))->toBe(count($endGraphs)); + expect(count($initialViews))->toBe(count($endViews)); - $graphs = $this->schemaManager->getGraphs(); - expect(count($graphs))->toBe(0); - - $views = $this->schemaManager->getViews(); - expect(count($views))->toBe(2); }); test('migrate:fresh --drop-types', function () { diff --git a/tests/Console/WipeCommandTest.php b/tests/Console/WipeCommandTest.php index f41add2..49a398d 100644 --- a/tests/Console/WipeCommandTest.php +++ b/tests/Console/WipeCommandTest.php @@ -59,6 +59,8 @@ }); test('db:wipe --drop-analyzers', function () { + $initialAnalyzers = $this->schemaManager->getAnalyzers(); + $schemaManager = $this->connection->getArangoClient()->schema(); if (!$schemaManager->hasAnalyzer('dropMyAnalyzer')) { Schema::createAnalyzer( @@ -71,8 +73,8 @@ '--drop-analyzers' => true, ])->assertExitCode(0); - $analyzers = $this->schemaManager->getAnalyzers(); - expect(count($analyzers))->toBe(13); + $endAnalyzers = $this->schemaManager->getAnalyzers(); + expect(count($initialAnalyzers))->toBe(count($endAnalyzers)); }); test('db:wipe --drop-graphs', function () { @@ -103,6 +105,10 @@ }); test('db:wipe --drop-all', function () { + $initialAnalyzers = $this->schemaManager->getAnalyzers(); + $initialViews = $this->schemaManager->getViews(); + $initialGraphs = $this->schemaManager->getGraphs(); + $schemaManager = $this->connection->getArangoClient()->schema(); if (!$schemaManager->hasAnalyzer('dropMyAnalyzer')) { Schema::createAnalyzer( @@ -135,14 +141,14 @@ '--drop-all' => true, ])->assertExitCode(0); - $analyzers = $this->schemaManager->getAnalyzers(); - expect(count($analyzers))->toBe(13); + $endAnalyzers = $this->schemaManager->getAnalyzers(); + $endGraphs = $this->schemaManager->getGraphs(); + $endViews = $this->schemaManager->getViews(); - $graphs = $this->schemaManager->getGraphs(); - expect(count($graphs))->toBe(0); - - $views = $this->schemaManager->getViews(); - expect(count($views))->toBe(0); + expect(count($initialAnalyzers))->toBe(count($endAnalyzers)); + expect(count($initialGraphs))->toBe(count($endGraphs)); + expect(count($initialViews))->toBe(2); + expect(count($endViews))->toBe(0); }); test('db:wipe --drop-types', function () { diff --git a/tests/Pest.php b/tests/Pest.php index 7c223d6..63ed5ce 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -54,9 +54,9 @@ /** @link https://pestphp.com/docs/helpers */ -function getBuilder() +function getBuilder($connection) { - $grammar = new Grammar(); + $grammar = new Grammar($connection); $processor = m::mock(Processor::class); return new Builder(m::mock(Connection::class), $grammar, $processor); diff --git a/tests/Query/GrammarTest.php b/tests/Query/GrammarTest.php index 644e928..b35630c 100644 --- a/tests/Query/GrammarTest.php +++ b/tests/Query/GrammarTest.php @@ -1,7 +1,7 @@ connection); $builder = $builder->select(['id', '_id', 'email']) ->from('users'); @@ -12,7 +12,7 @@ }); test('wrap bypass', function () { - $builder = getBuilder(); + $builder = getBuilder($this->connection); $builder = $builder->select('*') ->from('users') ->where('i`d', '=', "a123"); diff --git a/tests/Query/IdKeyConversionTest.php b/tests/Query/IdKeyConversionTest.php index c659147..c6758fe 100644 --- a/tests/Query/IdKeyConversionTest.php +++ b/tests/Query/IdKeyConversionTest.php @@ -24,7 +24,7 @@ }); test('get id conversion single attribute', function () { - $builder = getBuilder(); + $builder = getBuilder($this->connection); $builder = $builder->select('id')->from('users'); $this->assertSame( diff --git a/tests/Query/InsertTest.php b/tests/Query/InsertTest.php index 1f599c0..fe6be9f 100644 --- a/tests/Query/InsertTest.php +++ b/tests/Query/InsertTest.php @@ -25,7 +25,7 @@ test('insert get id', function () { - $builder = getBuilder(); + $builder = getBuilder($this->connection); $builder->getConnection()->shouldReceive('execute')->once()->andReturn(1); $result = $builder->from('users')->insertGetId(['email' => 'foo']); diff --git a/tests/Query/OrderingTest.php b/tests/Query/OrderingTest.php index f9648b2..3ce2a2c 100644 --- a/tests/Query/OrderingTest.php +++ b/tests/Query/OrderingTest.php @@ -5,7 +5,7 @@ use Illuminate\Support\Facades\DB; test('orderBy', function () { - $builder = getBuilder(); + $builder = getBuilder($this->connection); $builder->select('*')->from('users')->orderBy('email')->orderBy('age', 'desc'); $this->assertSame( 'FOR userDoc IN users SORT `userDoc`.`email` ASC, `userDoc`.`age` DESC RETURN userDoc', @@ -46,7 +46,7 @@ }); test('orderByRaw', function () { - $builder = getBuilder(); + $builder = getBuilder($this->connection); $builder->select('*')->from('users')->orderByRaw('userDoc.age @direction', ['@direction' => 'ASC']); $this->assertSame( 'FOR userDoc IN users SORT userDoc.age @direction RETURN userDoc', @@ -56,7 +56,7 @@ test('reorder', function () { - $builder = getBuilder(); + $builder = getBuilder($this->connection); $builder->select('*') ->from('users') ->orderByRaw('userDoc.age @direction', ['@direction' => 'ASC']) diff --git a/tests/Query/WheresTest.php b/tests/Query/WheresTest.php index d243b7b..522f296 100644 --- a/tests/Query/WheresTest.php +++ b/tests/Query/WheresTest.php @@ -4,7 +4,7 @@ use TestSetup\Models\Character; test('basic wheres', function () { - $builder = getBuilder(); + $builder = getBuilder($this->connection); $builder = $builder->select('*') ->from('users') ->where('id', '=', "a123"); @@ -18,7 +18,7 @@ }); test('basic wheres with multiple predicates', function () { - $builder = getBuilder(); + $builder = getBuilder($this->connection); $builder->select('*') ->from('users') ->where('id', '=', 1) @@ -35,7 +35,7 @@ }); test('basic or wheres', function () { - $builder = getBuilder(); + $builder = getBuilder($this->connection); $builder->select('*') ->from('users') ->where('id', '==', 1) @@ -50,7 +50,7 @@ }); test('where operator conversion', function () { - $builder = getBuilder(); + $builder = getBuilder($this->connection); $builder->select('*') ->from('users') ->where('email', '=', 'email@example.com') @@ -68,7 +68,7 @@ }); test('where =~ operator', function () { - $builder = getBuilder(); + $builder = getBuilder($this->connection); $builder->select('*') ->from('users') ->where('email', '=~', 'email@example.com'); @@ -83,7 +83,7 @@ }); test('where json arrow conversion', function () { - $builder = getBuilder(); + $builder = getBuilder($this->connection); $builder->select('*') ->from('users') ->where('email->address', '=', 'email@example.com') @@ -101,7 +101,7 @@ }); test('where json contains', function () { - $builder = getBuilder(); + $builder = getBuilder($this->connection); $builder->select('*') ->from('users') ->whereJsonContains('options->languages', 'en'); @@ -116,7 +116,7 @@ }); test('where json length', function () { - $builder = getBuilder(); + $builder = getBuilder($this->connection); $builder->select('*') ->from('users') ->whereJsonLength('options->languages', '>', 'en'); @@ -131,7 +131,7 @@ }); test('where between', function () { - $builder = getBuilder(); + $builder = getBuilder($this->connection); $builder->select('*')->from('users')->whereBetween('votes', [1, 100]); $this->assertSame( @@ -145,7 +145,7 @@ }); test('where not between', function () { - $builder = getBuilder(); + $builder = getBuilder($this->connection); $builder->select('*')->from('users')->whereNotBetween('votes', [1, 100]); $this->assertSame( @@ -159,7 +159,7 @@ }); test('where between columns', function () { - $builder = getBuilder(); + $builder = getBuilder($this->connection); $builder->select('*')->from('users')->whereBetweenColumns('votes', ['min_vote', 'max_vote']); $this->assertSame( @@ -170,7 +170,7 @@ }); test('where column', function () { - $builder = getBuilder(); + $builder = getBuilder($this->connection); $builder->select('*')->from('users')->whereColumn('first_name', '=', 'last_name'); $this->assertSame( @@ -180,7 +180,7 @@ }); test('where column without operator', function () { - $builder = getBuilder(); + $builder = getBuilder($this->connection); $builder->select('*')->from('users')->whereColumn('first_name', 'last_name'); $this->assertSame( @@ -190,12 +190,12 @@ }); test('where nulls', function () { - $builder = getBuilder(); + $builder = getBuilder($this->connection); $builder->select('*')->from('users')->whereNull('_key'); expect($builder->toSql())->toBe('FOR userDoc IN users FILTER `userDoc`.`_key` == null RETURN userDoc'); expect($builder->getBindings())->toEqual([]); - $builder = getBuilder(); + $builder = getBuilder($this->connection); $builder->select('*') ->from('users') ->where('id', '=', 1) @@ -210,12 +210,12 @@ }); test('where not nulls', function () { - $builder = getBuilder(); + $builder = getBuilder($this->connection); $builder->select('*')->from('users')->whereNotNull('id'); expect($builder->toSql())->toBe('FOR userDoc IN users FILTER `userDoc`.`_key` != null RETURN userDoc'); expect($builder->getBindings())->toEqual([]); - $builder = getBuilder(); + $builder = getBuilder($this->connection); $builder->select('*') ->from('users') ->where('id', '>', 1) @@ -254,7 +254,7 @@ test('where integer in raw', function () { - $builder = getBuilder(); + $builder = getBuilder($this->connection); $builder->select() ->from('users') @@ -267,7 +267,7 @@ }); test('where not in', function () { - $builder = getBuilder(); + $builder = getBuilder($this->connection); $builder->select() ->from('users') @@ -282,7 +282,7 @@ }); test('where integer not in raw', function () { - $builder = getBuilder(); + $builder = getBuilder($this->connection); $builder->select() ->from('users') @@ -295,7 +295,7 @@ }); test('where date', function () { - $builder = getBuilder(); + $builder = getBuilder($this->connection); $builder->select('*')->from('users')->whereDate('created_at', '2016-12-31'); $this->assertSame( @@ -307,7 +307,7 @@ }); test('where year', function () { - $builder = getBuilder(); + $builder = getBuilder($this->connection); $builder->select('*')->from('users')->whereYear('created_at', '2016'); $this->assertSame( @@ -319,7 +319,7 @@ }); test('where month', function () { - $builder = getBuilder(); + $builder = getBuilder($this->connection); $builder->select('*')->from('users')->whereMonth('created_at', '12'); $this->assertSame( @@ -331,7 +331,7 @@ }); test('where day', function () { - $builder = getBuilder(); + $builder = getBuilder($this->connection); $builder->select('*')->from('users')->whereDay('created_at', '31'); $this->assertSame( @@ -343,7 +343,7 @@ }); test('where time', function () { - $builder = getBuilder(); + $builder = getBuilder($this->connection); $builder->select('*')->from('users')->whereTime('created_at', '11:20:45'); $this->assertSame( @@ -427,7 +427,7 @@ }); test('where nested', function () { - $builder = getBuilder(); + $builder = getBuilder($this->connection); $query = $builder->select('*') ->from('characters') @@ -575,7 +575,7 @@ test('basic whereNot', function () { - $builder = getBuilder(); + $builder = getBuilder($this->connection); $builder->select('*')->from('characters')->where('surname', 'Lannister')->whereNot('alive', true); $this->assertSame( @@ -589,7 +589,7 @@ }); test('whereNot nested', function () { - $query = getBuilder(); + $query = getBuilder($this->connection); $query = $query ->select('*') ->from('characters') @@ -624,7 +624,7 @@ }); test('basic orWhereNot', function () { - $builder = getBuilder(); + $builder = getBuilder($this->connection); $builder->select('*')->from('characters')->where('alive', true)->orWhereNot('surname', 'Lannister'); $this->assertSame( diff --git a/tests/Schema/AnalyzerTest.php b/tests/Schema/AnalyzerTest.php index b662f5b..d96149c 100644 --- a/tests/Schema/AnalyzerTest.php +++ b/tests/Schema/AnalyzerTest.php @@ -21,10 +21,12 @@ test('getAnalyzers', function () { $schemaManager = $this->connection->getArangoClient()->schema(); + $initialAnalyzers = $schemaManager->getAnalyzers(); $analyzers = Schema::getAnalyzers(); - expect($analyzers)->toHaveCount(13); + $endAnalyzers = $schemaManager->getAnalyzers(); + expect(count($initialAnalyzers))->toBe(count($endAnalyzers)); }); test('replaceAnalyzer', function () { diff --git a/tests/Schema/ColumnTest.php b/tests/Schema/ColumnTest.php index 1ae2f5f..6bcde80 100644 --- a/tests/Schema/ColumnTest.php +++ b/tests/Schema/ColumnTest.php @@ -4,7 +4,7 @@ use Mockery as M; beforeEach(function () { - $this->grammar = new Grammar(); + $this->grammar = new Grammar($this->connection); }); afterEach(function () { diff --git a/tests/Schema/SchemaBuilderTest.php b/tests/Schema/SchemaBuilderTest.php index 858c12b..2ba9a01 100644 --- a/tests/Schema/SchemaBuilderTest.php +++ b/tests/Schema/SchemaBuilderTest.php @@ -228,10 +228,11 @@ test('getAnalyzers', function () { $schemaManager = $this->connection->getArangoClient()->schema(); + $initialAnalyzers = $schemaManager->getAnalyzers(); $analyzers = Schema::getAnalyzers(); - expect($analyzers)->toHaveCount(13); + expect(count($initialAnalyzers))->toBe(count($analyzers)); }); test('replaceAnalyzer', function () { From 09fa4a90748228a7502ef217fea2dc8b86ae3804 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 May 2025 03:55:17 +0000 Subject: [PATCH 34/34] chore(deps): bump dependabot/fetch-metadata from 2.3.0 to 2.4.0 Bumps [dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata) from 2.3.0 to 2.4.0. - [Release notes](https://github.com/dependabot/fetch-metadata/releases) - [Commits](https://github.com/dependabot/fetch-metadata/compare/v2.3.0...v2.4.0) --- updated-dependencies: - dependency-name: dependabot/fetch-metadata dependency-version: 2.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/dependabot-auto-merge.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index 2cb1d5e..1a13177 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -13,7 +13,7 @@ jobs: - name: Dependabot metadata id: metadata - uses: dependabot/fetch-metadata@v2.3.0 + uses: dependabot/fetch-metadata@v2.4.0 with: github-token: "${{ secrets.GITHUB_TOKEN }}"