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/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml
index 611344a..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.1.0
+ uses: dependabot/fetch-metadata@v2.4.0
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
index 43e8e33..a17c471 100644
--- a/.github/workflows/run-tests.yml
+++ b/.github/workflows/run-tests.yml
@@ -16,11 +16,11 @@ jobs:
strategy:
matrix:
arangodb: ['3.11', '3.12']
- php: ['8.2', '8.3']
- laravel: ['^11.0']
+ php: ['8.2', '8.3', '8.4']
+ 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/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..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
@@ -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.
*
@@ -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/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..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
@@ -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.
*
@@ -16,7 +16,11 @@ public function up()
{
Schema::create('taggables', function (Blueprint $collection) {
//
- });
+ }, [
+ 'keyOptions' => [
+ 'type' => 'padded',
+ ],
+ ]);
}
/**
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
new file mode 100644
index 0000000..58c1c92
--- /dev/null
+++ b/TestSetup/Database/Migrations/2024_08_05_145621_create_events_table.php
@@ -0,0 +1,31 @@
+call(UsersSeeder::class);
$this->call(CharactersSeeder::class);
$this->call(ChildrenSeeder::class);
$this->call(LocationsSeeder::class);
$this->call(TagsSeeder::class);
$this->call(TaggablesSeeder::class);
$this->call(HousesSeeder::class);
+ $this->call(EventsSeeder::class);
}
}
diff --git a/TestSetup/Database/Seeders/EventsSeeder.php b/TestSetup/Database/Seeders/EventsSeeder.php
new file mode 100644
index 0000000..44ca194
--- /dev/null
+++ b/TestSetup/Database/Seeders/EventsSeeder.php
@@ -0,0 +1,56 @@
+ '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/compatibility-list.md b/docs/compatibility-list.md
index 4d59674..cc86823 100644
--- a/docs/compatibility-list.md
+++ b/docs/compatibility-list.md
@@ -58,23 +58,23 @@ groupByRaw
### Unions
union / unionAll
-#### Unsupported union clauses
-Union orders / Union aggregates / Union groupBy
-
### Expressions
Expression / raw
### Joins
-crossJoin / join / joinSub? / leftJoin / leftJoinSub?
+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
whereBetween / whereNotBetween / whereBetweenColumns / whereNotBetweenColumns /
whereJsonContains / whereJsonLength /
-whereIn / whereNotIn / whereNull / whereNotNull /
+whereIn / whereNotIn /
+whereLike / orWhereLike / whereNotLike / orWhereNotLike /
+whereNone / orWhereNone / whereNot / orWhereNot /
+whereNull / whereNotNull /
whereDate / whereMonth / whereDay / whereYear / whereTime /
whereRaw (use AQL) /
whereAll / orWhereAll / whereAny / orWhereAny
@@ -100,7 +100,7 @@ limit / offset / take / skip
when
### Insert statements
-insert / insertOrIgnore / insertUsing / insertGetId
+insert / insertOrIgnore / insertUsing / insertOrIgnoreUsing / insertGetId
### Update statements
update / updateOrInsert / upsert /
@@ -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 / 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,31 +142,36 @@ 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?
+
+#### Querying Relationship Existence
+has / orHas / whereHas / orWhereHas / whereRelation / orWhereRelation /
+whereMorphRelation / orWhereMorphRelation
-enforceMorphMap / getMorphClass / getMorphedModel / resolveRelationUsing
+#### Querying Relationship Absence
+doesntHave / orDoesntHave /
+whereDoesntHave / orWhereDoesntHave / whereDoesntHaveRelation / orWhereDoesntHaveRelation /
+whereMorphDoesntHaveRelation / orWhereMorphDoesntHaveRelation
-#### Query relationships
-has / orHas / whereHas / whereRelation / doesntHave /
-whereDoesntHave / whereHasMorph / whereDoesntHaveMorph
+#### Querying Morph To Relationships
+whereHasMorph / orWhereHasMorph / whereDoesntHaveMorph / orWhereDoesntHaveMorph / whereMorphedTo? / whereNotMorphedTo?
#### Aggregating related models
-withCount / loadCount /
-withSum / loadSum / withExists / morphWithCount /loadMorphCount /
+withCount / loadCount? /
+withSum? / loadSum? / withExists / morphWithCount? /loadMorphCount? /
+loadMorphCount?
-loadMorphCount
#### Eager loading
with / without / withOnly / constrain /
load / loadMissing / loadMorph / preventLazyLoading
@@ -180,14 +185,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
@@ -228,8 +233,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)
diff --git a/docs/console-commands.md b/docs/console-commands.md
new file mode 100644
index 0000000..0d5c781
--- /dev/null
+++ b/docs/console-commands.md
@@ -0,0 +1,64 @@
+# 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
+
+## 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
+
+## 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**_
+
+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/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/docs/migrations.md b/docs/migrations.md
index 329f1a2..a5b36e2 100644
--- a/docs/migrations.md
+++ b/docs/migrations.md
@@ -21,14 +21,15 @@ See the [ArangoDB Documentation for all options](https://docs.arangodb.com/3.3/H
Within the collection blueprint you can create indexes.
This following indexes are supported:
-Type | Purpose | Blueprint Method
----------- |-----------------------------| ----------------
-Persistent | Ranged matching | `$table->index($columns = null, $name = null, $algorithm = null, $indexOptions = [])`
-Primary * | Unique ranged matching | `$table->primary($columns = null, $name = null, $indexOptions = [])`
-Unique | Unique ranged matching | `$table->unique($attributes, $indexOptions = [])`
-Geo | Location matching | `$table->spatialIndex($columns, $name = null, $indexOptions = [])`
-TTL | Auto-expiring documents | `$table->ttlIndex($columns, $expireAfter, $name = null, $indexOptions = [])`
-Inverted | Fast full text searching | `$table->invertedIndex($columns = null, $name = null, $indexOptions = [])`
+Type | Purpose | Blueprint Method
+---------- |--------------------------| ----------------
+Persistent | Ranged matching | `$table->index($columns = null, $name = null, $algorithm = null, $indexOptions = [])`
+Primary * | Unique ranged matching | `$table->primary($columns = null, $name = null, $indexOptions = [])`
+Unique | Unique ranged matching | `$table->unique($attributes, $indexOptions = [])`
+Geo | Location matching | `$table->spatialIndex($columns, $name = null, $indexOptions = [])`
+TTL | Auto-expiring documents | `$table->ttlIndex($columns, $expireAfter, $name = null, $indexOptions = [])`
+Inverted | Fast full text searching | `$table->invertedIndex($columns = null, $name = null, $indexOptions = [])`
+multiDimensional | 2D+ numeric search | `$table->multiDimensionalIndex($columns = null, $name = null, $indexOptions = [], $type = 'mdi')`
* the primary method is supported for composite keys. ArangoDB already sets a primary index on the _key property.
@@ -87,17 +88,63 @@ Schema::dropView($viewName);
## Analyzers (ArangoSearch)
You can create, edit or delete an ArangoDB Analyzer.
-### New Analyzer
+### New analyzer
```php
-Schema::createAnalyzer($name, $config);
+Schema::createAnalyzer($name, $type, $properties, $features);
```
-### Replace Analyzer
+### Replace analyzer
```php
-Schema::replaceAnalyzer($name, $config);
+Schema::replaceAnalyzer($name, $type, $properties, $features);
```
-### Delete Analyzer
+### Delete analyzer
```php
Schema::dropAnalyzer($name);
-```
\ No newline at end of file
+```
+
+### Delete all analyzers
+```php
+Schema::dropAnalyzers($name);
+```
+
+## 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::getGraphs();
+```
+
+### Delete a graph
+```php
+Schema::dropGraph($name);
+```
+
+### Delete a graph if it exists
+```php
+Schema::dropGraphIfExists($name);
+```
+
+### Delete all graphs
+```php
+Schema::dropGraphs();
+```
\ No newline at end of file
diff --git a/phpstan.neon b/phpstan.neon
index 8adfaa3..0e502c6 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -2,11 +2,15 @@ includes:
- vendor/larastan/larastan/extension.neon
parameters:
- level: 8
- checkGenericClassInNonGenericObjectType: false
- universalObjectCratesClasses:
- - 'Illuminate\Support\Fluent'
- paths:
- - src
- excludePaths:
- - src/Schema
\ No newline at end of file
+ level: 8
+ ignoreErrors:
+ - identifier: missingType.generics
+ - identifier: trait.unused
+ universalObjectCratesClasses:
+ - 'Illuminate\Support\Fluent'
+ paths:
+ - src
+ excludePaths:
+ - 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 8adfaa3..0e502c6 100644
--- a/phpstan.neon.dist
+++ b/phpstan.neon.dist
@@ -2,11 +2,15 @@ includes:
- vendor/larastan/larastan/extension.neon
parameters:
- level: 8
- checkGenericClassInNonGenericObjectType: false
- universalObjectCratesClasses:
- - 'Illuminate\Support\Fluent'
- paths:
- - src
- excludePaths:
- - src/Schema
\ No newline at end of file
+ level: 8
+ ignoreErrors:
+ - identifier: missingType.generics
+ - identifier: trait.unused
+ universalObjectCratesClasses:
+ - 'Illuminate\Support\Fluent'
+ paths:
+ - src
+ excludePaths:
+ - src/Schema
+ - src/Facades/Schema.php
+ treatPhpDocTypesAsCertain: false
\ No newline at end of file
diff --git a/phpunit.xml b/phpunit.xml
index 146617a..105e788 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -29,7 +29,8 @@
-
+
+
diff --git a/readme.md b/readme.md
index ddd636e..2e9ba45 100644
--- a/readme.md
+++ b/readme.md
@@ -18,15 +18,27 @@ 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:
+
+```
+ "minimum-stability": "beta",
+ "prefer-stable": false,
+```
+
+You may then use composer to install Aranguent:
``` composer require laravel-freelancer-nl/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 a4121de..f450b9f 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
{
@@ -26,10 +28,12 @@ class AranguentServiceProvider extends ServiceProvider
*/
public function boot()
{
+ /** @phpstan-ignore offsetAccess.nonOffsetAccessible */
if (isset($this->app['db'])) {
Model::setConnectionResolver($this->app['db']);
}
+ /** @phpstan-ignore offsetAccess.nonOffsetAccessible */
if (isset($this->app['events'])) {
Model::setEventDispatcher($this->app['events']);
}
@@ -56,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) {
@@ -64,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 383a6f7..28cae0e 100644
--- a/src/Concerns/RunsQueries.php
+++ b/src/Concerns/RunsQueries.php
@@ -11,9 +11,21 @@
use LaravelFreelancerNL\Aranguent\Exceptions\QueryException;
use LaravelFreelancerNL\FluentAQL\QueryBuilder as FluentAqlBuilder;
use stdClass;
+use LaravelFreelancerNL\Aranguent\Exceptions\UniqueConstraintViolationException;
trait RunsQueries
{
+ /**
+ * Determine if the given database exception was caused by a unique constraint violation.
+ *
+ * @param \Exception $exception
+ * @return bool
+ */
+ protected function isUniqueConstraintError(Exception $exception)
+ {
+ return boolval(preg_match('/409 - AQL: unique constraint violated/i', $exception->getMessage()));
+ }
+
/**
* Run a select statement against the database and returns a generator.
*
@@ -21,7 +33,7 @@ trait RunsQueries
* @param array $bindings
* @param bool $useReadPdo
* @return \Generator
- * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
+ * @SuppressWarnings("PHPMD.BooleanArgumentFlag")
*/
public function cursor($query, $bindings = [], $useReadPdo = true)
{
@@ -183,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
@@ -198,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
@@ -283,7 +295,7 @@ protected function run($query, $bindings, Closure $callback)
$this->logQuery(
$query,
$bindings,
- $this->getElapsedTime((int) $start),
+ $this->getElapsedTime($start),
);
return $result;
@@ -310,6 +322,15 @@ protected function runQueryCallback($query, $bindings, Closure $callback)
// message to include the bindings with SQL, which will make this exception a
// lot more helpful to the developer instead of just the database's errors.
+ if ($this->isUniqueConstraintError($e)) {
+ throw new UniqueConstraintViolationException(
+ (string) $this->getName(),
+ $query,
+ $this->prepareBindings($bindings),
+ $e,
+ );
+ }
+
throw new QueryException(
(string) $this->getName(),
$query,
diff --git a/src/Connection.php b/src/Connection.php
index bf2584b..3ca2992 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.
@@ -90,7 +93,7 @@ protected function getDefaultPostProcessor(): Processor
*/
protected function getDefaultQueryGrammar()
{
- ($grammar = new QueryGrammar())->setConnection($this);
+ $grammar = new QueryGrammar($this);
return $grammar;
}
@@ -198,11 +201,11 @@ protected function escapeBinary($value)
* @param bool $binary
* @return string
*
- * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
+ * @SuppressWarnings("PHPMD.BooleanArgumentFlag")
*/
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,
@@ -218,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)
{
@@ -249,7 +252,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);
}
@@ -282,4 +285,34 @@ 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();
+ }
+
+ /**
+ * 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/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/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/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/ShowCommand.php b/src/Console/ShowCommand.php
new file mode 100644
index 0000000..5da75d1
--- /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/Console/ShowModelCommand.php b/src/Console/ShowModelCommand.php
new file mode 100644
index 0000000..35b6d10
--- /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) => (isset($attribute[$property])) ? $attribute[$property] : false)
+ ->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/Console/TableCommand.php b/src/Console/TableCommand.php
new file mode 100644
index 0000000..ff620ff
--- /dev/null
+++ b/src/Console/TableCommand.php
@@ -0,0 +1,221 @@
+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;
+ }
+
+ [$columns, $indexes] = $connection->withoutTablePrefix(function ($connection) use ($table) {
+ $schema = $connection->getSchemaBuilder();
+ $tableName = $table['name'];
+
+ return [
+ $this->columns($schema, $tableName),
+ $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['type']),
+ );
+ });
+ $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/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/Eloquent/Builder.php b/src/Eloquent/Builder.php
index 3e95c91..ad841bb 100644
--- a/src/Eloquent/Builder.php
+++ b/src/Eloquent/Builder.php
@@ -7,6 +7,7 @@
use Illuminate\Database\Eloquent\Builder as IlluminateEloquentBuilder;
use Illuminate\Support\Arr;
use LaravelFreelancerNL\Aranguent\Eloquent\Concerns\QueriesAranguentRelationships;
+use LaravelFreelancerNL\Aranguent\Exceptions\UniqueConstraintViolationException;
use LaravelFreelancerNL\Aranguent\Query\Builder as QueryBuilder;
class Builder extends IlluminateEloquentBuilder
@@ -20,6 +21,22 @@ class Builder extends IlluminateEloquentBuilder
*/
protected $query;
+ /**
+ * Attempt to create the record. If a unique constraint violation occurs, attempt to find the matching record.
+ *
+ * @param mixed[] $attributes
+ * @param mixed[] $values
+ * @return \Illuminate\Database\Eloquent\Model|static
+ */
+ public function createOrFirst(array $attributes = [], array $values = [])
+ {
+ try {
+ return $this->withSavepointIfNeeded(fn() => $this->create(array_merge($attributes, $values)));
+ } catch (UniqueConstraintViolationException $e) {
+ return $this->useWritePdo()->where($attributes)->first() ?? throw $e;
+ }
+ }
+
/**
* Insert a record in the database.
*
diff --git a/src/Eloquent/Casts/AsArrayObject.php b/src/Eloquent/Casts/AsArrayObject.php
index a956d74..b57203c 100644
--- a/src/Eloquent/Casts/AsArrayObject.php
+++ b/src/Eloquent/Casts/AsArrayObject.php
@@ -17,11 +17,11 @@ 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)
{
- return new class () implements CastsAttributes {
+ return new class implements CastsAttributes {
public function get($model, $key, $value, $attributes)
{
if (! isset($attributes[$key])) {
@@ -31,7 +31,7 @@ public function get($model, $key, $value, $attributes)
$data = $attributes[$key];
if (is_object($data)) {
- $data = (array) $data;
+ $data = mapObjectToArray($data);
}
return is_array($data) ? new ArrayObject($data, ArrayObject::ARRAY_AS_PROPS) : null;
@@ -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 e073351..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)
{
@@ -33,6 +33,7 @@ public function fromJson($value, $asObject = false)
return (object) $value;
}
- return (array) $value;
+ // Recursively cast objects within the value to arrays
+ return mapObjectToArray($value);
}
}
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/Eloquent/Concerns/QueriesAranguentRelationships.php b/src/Eloquent/Concerns/QueriesAranguentRelationships.php
index 7f50367..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)
{
@@ -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);
@@ -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/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/Eloquent/Relations/Concerns/IsAranguentRelation.php b/src/Eloquent/Relations/Concerns/IsAranguentRelation.php
index d0b0537..12448eb 100644
--- a/src/Eloquent/Relations/Concerns/IsAranguentRelation.php
+++ b/src/Eloquent/Relations/Concerns/IsAranguentRelation.php
@@ -5,10 +5,31 @@
namespace LaravelFreelancerNL\Aranguent\Eloquent\Relations\Concerns;
use Illuminate\Database\Eloquent\Builder;
-use Illuminate\Database\Query\Expression;
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.
*
@@ -19,7 +40,6 @@ public function getRelationExistenceCountQuery(Builder $query, Builder $parentQu
return $this->getRelationExistenceQuery(
$query,
$parentQuery,
- new Expression('*'),
);
}
}
diff --git a/src/Exceptions/QueryException.php b/src/Exceptions/QueryException.php
index d665189..e9da997 100644
--- a/src/Exceptions/QueryException.php
+++ b/src/Exceptions/QueryException.php
@@ -10,10 +10,12 @@
class QueryException extends IlluminateQueryException
{
/**
- * Format the SQL error message.
+ * Create a new query exception instance.
*
+ * @param string $connectionName
* @param string $sql
- * @param array $bindings
+ * @param mixed[] $bindings
+ * @param \Throwable $previous
* @return string
*/
protected function formatMessage($connectionName, $sql, $bindings, Throwable $previous)
diff --git a/src/Exceptions/UniqueConstraintViolationException.php b/src/Exceptions/UniqueConstraintViolationException.php
new file mode 100644
index 0000000..8f2faea
--- /dev/null
+++ b/src/Exceptions/UniqueConstraintViolationException.php
@@ -0,0 +1,7 @@
+useFallback()) {
diff --git a/src/Providers/CommandServiceProvider.php b/src/Providers/CommandServiceProvider.php
index d73f802..cfacb90 100644
--- a/src/Providers/CommandServiceProvider.php
+++ b/src/Providers/CommandServiceProvider.php
@@ -4,6 +4,10 @@
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;
use Illuminate\Database\Console\DbCommand as IlluminateDbCommand;
use LaravelFreelancerNL\Aranguent\Console\ModelMakeCommand;
@@ -21,6 +25,10 @@ class CommandServiceProvider extends ServiceProvider
protected $commands = [
'ModelMake' => ModelMakeCommand::class,
'Db' => DbCommand::class,
+ 'DbWipe' => WipeCommand::class,
+ 'DbShow' => ShowCommand::class,
+ 'DbTable' => TableCommand::class,
+ 'ShowModel' => ShowModelCommand::class,
];
@@ -39,27 +47,35 @@ 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));
}
- 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/src/Providers/MigrationServiceProvider.php b/src/Providers/MigrationServiceProvider.php
index 919b412..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,
];
/**
@@ -40,7 +42,7 @@ public function __construct($app)
{
parent::__construct($app);
- foreach($this->aliases as $key => $alias) {
+ foreach ($this->aliases as $key => $alias) {
$this->aliases[$key] = $alias;
}
}
@@ -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/Query/Builder.php b/src/Query/Builder.php
index 58cdf02..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,
- IlluminateQueryGrammar $grammar = null,
- IlluminateProcessor $processor = null,
- AQB $aqb = null,
+ 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 8929433..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;
}
@@ -144,7 +146,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;
}
@@ -160,9 +162,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 +176,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');
@@ -197,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 989a4cd..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
{
@@ -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/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/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/BuildsUpdates.php b/src/Query/Concerns/BuildsUpdates.php
index 56282c5..e2688bf 100644
--- a/src/Query/Concerns/BuildsUpdates.php
+++ b/src/Query/Concerns/BuildsUpdates.php
@@ -4,6 +4,7 @@
namespace LaravelFreelancerNL\Aranguent\Query\Concerns;
+use Closure;
use Illuminate\Database\Query\Expression;
use Illuminate\Support\Arr;
use InvalidArgumentException;
@@ -21,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);
@@ -64,13 +65,19 @@ public function update(array $values)
* Insert or update a record matching the attributes, and fill it with values.
*
* @param array $attributes
- * @param array $values
+ * @param array|callable $values
* @return bool
* @throws BindException
*/
- public function updateOrInsert(array $attributes, array $values = [])
+ public function updateOrInsert(array $attributes, array|callable $values = [])
{
- if (!$this->where($attributes)->exists()) {
+ $exists = $this->where($attributes)->exists();
+
+ if ($values instanceof Closure) {
+ $values = $values($exists);
+ }
+
+ if (! $exists) {
$this->bindings['where'] = [];
return $this->insert(array_merge($attributes, $values));
}
@@ -153,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..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)
{
@@ -92,7 +93,7 @@ public function validateOperator(mixed $operator, mixed $value): array
if ($this->invalidOperator($operator)) {
[$value, $operator] = [$operator, '=='];
}
- return array($value, $operator);
+ return [$value, $operator];
}
/**
@@ -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
@@ -435,6 +436,29 @@ 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
+ *
+ * @SuppressWarnings("PHPMD.BooleanArgumentFlag")
+ */
+ 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.
*
@@ -443,7 +467,7 @@ public function whereJsonLength($column, $operator, $value = null, $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 8270da8..fb0bca1 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.
*
@@ -43,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)
{
@@ -91,7 +124,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 +170,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 +191,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);
@@ -172,7 +205,6 @@ protected function normalizeColumnReferences(IlluminateQueryBuilder $query, stri
$references = explode('.', $column);
-
$tableAlias = $query->getTableAlias($references[0]);
if (isset($tableAlias)) {
@@ -184,6 +216,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]);
}
@@ -211,7 +244,7 @@ protected function cleanAlias(IlluminateQueryBuilder $query, int|null|string $al
$elements = explode('.', $alias);
- if(
+ if (
!$query->isTable($elements[0])
&& !$query->isVariable($elements[0])
) {
@@ -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 (
@@ -289,6 +313,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/CompilesDataManipulations.php b/src/Query/Concerns/CompilesDataManipulations.php
index 9d0a7b7..b7dc0f5 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);
@@ -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);
@@ -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
@@ -129,7 +164,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 +223,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/CompilesFilters.php b/src/Query/Concerns/CompilesFilters.php
index d38b770..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)
{
@@ -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.
*
@@ -496,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)
{
@@ -510,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)
{
@@ -524,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/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..264dda5 100644
--- a/src/Query/Concerns/CompilesJoins.php
+++ b/src/Query/Concerns/CompilesJoins.php
@@ -20,13 +20,16 @@ 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];
+
+ $query->registerTableAlias($join->table, $alias);
+
+ return [$table, $alias];
+ }
- return [$table, $alias];
}
$table = (string) $this->wrapTable($join->table);
diff --git a/src/Query/Concerns/CompilesUnions.php b/src/Query/Concerns/CompilesUnions.php
index 8bd5286..4870077 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);
@@ -33,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);
}
@@ -47,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/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..0d081c2 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);
@@ -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;
}
@@ -155,7 +159,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 +174,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);
@@ -182,12 +189,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 07eeaa0..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)
{
@@ -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;
@@ -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));
@@ -211,7 +211,6 @@ public function wrapSubquery(string $subquery): string
public function generateAqlObject(array $data): string
{
$data = Arr::undot($data);
- ray($this->generateAqlObjectString($data));
return $this->generateAqlObjectString($data);
}
@@ -221,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)) {
@@ -270,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,
@@ -290,7 +289,7 @@ public function substituteBindingsIntoRawSql($aql, $bindings)
*/
public function isJsonSelector($value)
{
- if(!is_string($value)) {
+ if (!is_string($value)) {
return false;
}
@@ -320,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);
}
@@ -334,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/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/Query/Grammar.php b/src/Query/Grammar.php
index aaeaadf..54dc477 100644
--- a/src/Query/Grammar.php
+++ b/src/Query/Grammar.php
@@ -55,11 +55,10 @@ class Grammar extends IlluminateQueryGrammar
*/
protected $operators = [
'==', '!=', '<', '>', '<=', '>=',
- 'LIKE', '~', '!~',
- 'IN', 'NOT IN',
- 'ALL ==', 'ALL !=', 'ALL <', 'ALL >', 'ALL <=', 'ALL >=', 'ALL IN',
- 'ANY ==', 'ANY !=', 'ANY <', 'ANY >', 'ANY <=', 'ANY >=', 'ANY IN',
- 'NONE ==', 'NONE !=', 'NONE <', 'NONE >', 'NONE <=', 'NONE >=', 'NONE IN',
+ 'IN', 'NOT IN', 'LIKE', 'NOT LIKE', '=~', '!~',
+ 'ALL ==', 'ALL !=', 'ALL <', 'ALL >', 'ALL <=', 'ALL >=', 'ALL IN', 'ALL NOT IN',
+ 'ANY ==', 'ANY !=', 'ANY <', 'ANY >', 'ANY <=', 'ANY >=', 'ANY IN', 'ANY NOT IN',
+ 'NONE ==', 'NONE !=', 'NONE <', 'NONE >', 'NONE <=', 'NONE >=', 'NONE IN', 'NONE NOT IN',
];
/**
@@ -176,6 +175,10 @@ protected function compileComponents(IlluminateQueryBuilder $query)
continue;
}
+ if ($component === 'aggregate' && $query->unions !== null) {
+ continue;
+ }
+
if (isset($query->$component)) {
$method = 'compile' . ucfirst($component);
@@ -203,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 = ['*'];
}
@@ -217,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();
}
@@ -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)
{
@@ -420,7 +420,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/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/Blueprint.php b/src/Schema/Blueprint.php
index 4d1eddc..a039b35 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.
@@ -282,13 +282,9 @@ public function __call($method, $args = [])
}
}
- $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);
@@ -296,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 = [];
@@ -318,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/Builder.php b/src/Schema/Builder.php
index 184d301..6e1be8f 100644
--- a/src/Schema/Builder.php
+++ b/src/Schema/Builder.php
@@ -12,12 +12,18 @@
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;
use LaravelFreelancerNL\Aranguent\Schema\Concerns\UsesBlueprints;
class Builder extends \Illuminate\Database\Schema\Builder
{
use HandlesAnalyzers;
+ use HandlesIndexNaming;
+ use HandlesGraphs;
+ use HandlesIndexes;
use HandlesViews;
use UsesBlueprints;
@@ -37,6 +43,17 @@ class Builder extends \Illuminate\Database\Schema\Builder
*/
public $grammar;
+
+ /**
+ * index prefixes?
+ */
+ public ?bool $prefixIndexes;
+
+ /**
+ * The table prefix.
+ */
+ public string $prefix;
+
/**
* Create a new database Schema manager.
*
@@ -49,6 +66,13 @@ public function __construct(Connection $connection)
$this->grammar = $connection->getSchemaGrammar();
$this->schemaManager = $connection->getArangoClient()->schema();
+
+ $this->prefixIndexes = $this->connection->getConfig('prefix_indexes');
+
+ $this->prefix = $this->prefixIndexes
+ ? $this->connection->getConfig('prefix')
+ : '';
+
}
/**
@@ -78,24 +102,44 @@ 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->mapResultsToArray(
+ $this->schemaManager->getCollections(false),
+ );
}
/**
* Get the tables that belong to the database.
*
- * @return array
+ * @param string|string[]|null $schema
+ * @return array
* @throws ArangoException
*/
- public function getTables()
+ public function getTables($schema = null)
{
- return $this->schemaManager->getCollections(true);
+ unset($schema);
+
+ return $this->mapResultsToArray(
+ $this->schemaManager->getCollections(true),
+ );
}
/**
@@ -128,10 +172,10 @@ public function drop($table)
*/
public function dropAllTables(): void
{
- $collections = $this->getAllTables();
+ $collections = $this->getTables(true);
foreach ($collections as $name) {
- $this->schemaManager->deleteCollection($name->name);
+ $this->schemaManager->deleteCollection($name['name']);
}
}
@@ -147,6 +191,22 @@ public function hasColumn($table, $column)
return $this->hasColumns($table, $column);
}
+ /**
+ * Determine if the given table has given columns.
+ *
+ * @param string $table
+ * @param string|string[] $columns
+ * @return array
+ */
+ public function getColumns($table)
+ {
+ $compilation = $this->grammar->compileColumns(null, $table);
+
+ $rawColumns = $this->connection->select($compilation['aqb'], $compilation['bindings']);
+
+ return $this->mapResultsToArray($rawColumns);
+ }
+
/**
* Determine if the given table has given columns.
*
@@ -237,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/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/HandlesAnalyzers.php b/src/Schema/Concerns/HandlesAnalyzers.php
index 99a5c31..3da7393 100644
--- a/src/Schema/Concerns/HandlesAnalyzers.php
+++ b/src/Schema/Concerns/HandlesAnalyzers.php
@@ -10,33 +10,48 @@ trait HandlesAnalyzers
{
/**
* @param array $properties
+ * @param array $features
*
* @throws ArangoException
*/
- public function createAnalyzer(string $name, array $properties)
+ public function createAnalyzer(string $name, string $type, array $properties = null, array $features = null)
{
- $analyzer = $properties;
- $analyzer['name'] = $name;
+ $analyzer = array_filter([
+ 'name' => $name,
+ 'type' => $type,
+ 'properties' => $properties,
+ 'features' => $features,
+ ]);
$this->schemaManager->createAnalyzer($analyzer);
}
/**
* @param array $properties
+ * @param array $features
*
* @throws ArangoException
*/
- public function replaceAnalyzer(string $name, array $properties)
+ public function replaceAnalyzer(string $name, string $type, array $properties = null, array $features = null)
{
- $this->schemaManager->replaceAnalyzer($name, $properties);
+ $analyzer = array_filter([
+ 'name' => $name,
+ 'type' => $type,
+ 'properties' => $properties,
+ 'features' => $features,
+ ]);
+
+ $this->schemaManager->replaceAnalyzer($name, $analyzer);
}
/**
+ * @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
@@ -49,9 +64,11 @@ public function hasAnalyzer(string $analyzer): bool
/**
* @throws ArangoException
*/
- public function getAllAnalyzers(): array
+ public function getAnalyzers(): array
{
- return $this->schemaManager->getAnalyzers();
+ return $this->mapResultsToArray(
+ $this->schemaManager->getAnalyzers(),
+ );
}
/**
@@ -73,4 +90,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..7557155
--- /dev/null
+++ b/src/Schema/Concerns/HandlesGraphs.php
@@ -0,0 +1,74 @@
+ $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);
+ });
+ }
+
+ /**
+ * @param string $name
+ * @return mixed[]
+ * @throws ArangoException
+ */
+ public function getGraph(string $name): array
+ {
+ return (array) $this->schemaManager->getGraph($name);
+ }
+
+ /**
+ * @return mixed[]
+ * @throws ArangoException
+ */
+ public function getGraphs(): array
+ {
+ return $this->mapResultsToArray(
+ $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/HandlesIndexNaming.php b/src/Schema/Concerns/HandlesIndexNaming.php
new file mode 100644
index 0000000..293ef47
--- /dev/null
+++ b/src/Schema/Concerns/HandlesIndexNaming.php
@@ -0,0 +1,43 @@
+prefix . ($table ?? $this->table);
+ $nameParts = array_merge($nameParts, $this->getColumnNames($columns));
+ $nameParts[] = $type;
+ $nameParts = array_merge($nameParts, array_keys($options));
+ array_filter($nameParts);
+
+ $index = strtolower(implode('_', $nameParts));
+ $index = preg_replace("/\[\*+\]+/", '_array', $index);
+
+ return preg_replace('/[^A-Za-z0-9]+/', '_', $index);
+ }
+
+ protected function getColumnNames(array $columns): array
+ {
+ $names = [];
+ foreach ($columns as $column) {
+ if (is_array($column) && $column['name'] !== '') {
+ $names[] = $column['name'];
+
+ continue;
+ }
+ $names[] = $column;
+ }
+
+ return $names;
+ }
+}
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/Concerns/HandlesViews.php b/src/Schema/Concerns/HandlesViews.php
index b3c67aa..a775a25 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,17 @@ public function hasView($view)
}
/**
+ * @param string|string[]|null $schema
+ * @return mixed[]
* @throws ArangoException
*/
- public function getAllViews(): array
+ public function getViews($schema = null): array
{
- return $this->schemaManager->getViews();
+ unset($schema);
+
+ return $this->mapResultsToArray(
+ $this->schemaManager->getViews(),
+ );
}
/**
@@ -85,13 +93,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/Schema/Concerns/Indexes.php b/src/Schema/Concerns/IndexCommands.php
similarity index 92%
rename from src/Schema/Concerns/Indexes.php
rename to src/Schema/Concerns/IndexCommands.php
index 2fe4415..ab87a9b 100644
--- a/src/Schema/Concerns/Indexes.php
+++ b/src/Schema/Concerns/IndexCommands.php
@@ -5,8 +5,10 @@
use ArangoClient\Exceptions\ArangoException;
use Illuminate\Support\Fluent;
-trait Indexes
+trait IndexCommands
{
+ use HandlesIndexNaming;
+
/**
* Add a new index command to the blueprint.
*
@@ -88,6 +90,17 @@ public function invertedIndex($columns = null, $name = null, $indexOptions = [])
return $this->indexCommand('inverted', $columns, $name, $indexOptions);
}
+
+ public function multiDimensionalIndex(array $columns = null, string $name = null, array $indexOptions = [], string $type = 'mdi'): Fluent
+ {
+ return $this->indexCommand(
+ $type,
+ $columns,
+ $name,
+ $indexOptions,
+ );
+ }
+
public function persistentIndex(array $columns = null, string $name = null, array $indexOptions = []): Fluent
{
return $this->indexCommand('persistent', $columns, $name, $indexOptions);
@@ -226,7 +239,13 @@ public function dropInvertedIndex(string $name): Fluent
return $this->dropIndex($name);
}
-
+ /**
+ * Indicate that the given index should be dropped.
+ */
+ public function dropMultiDimensionalIndex(string $name): Fluent
+ {
+ return $this->dropIndex($name);
+ }
/**
* Drop the index by first getting all the indexes on the table; then selecting the matching one
@@ -264,23 +283,4 @@ protected function mapIndexAlgorithm($algorithm): mixed
return (isset($algorithmConversion[$algorithm])) ? $algorithmConversion[$algorithm] : 'persistent';
}
- /**
- * Create a default index name for the table.
- *
- * @param string $type
- */
- public function createIndexName($type, array $columns, array $options = []): string
- {
- $nameParts = [];
- $nameParts[] = $this->prefix . $this->table;
- $nameParts = array_merge($nameParts, $columns);
- $nameParts[] = $type;
- $nameParts = array_merge($nameParts, array_keys($options));
- array_filter($nameParts);
-
- $index = strtolower(implode('_', $nameParts));
- $index = preg_replace("/\[\*+\]+/", '_array', $index);
-
- return preg_replace('/[^A-Za-z0-9]+/', '_', $index);
- }
}
diff --git a/src/Schema/Concerns/Tables.php b/src/Schema/Concerns/TableCommands.php
similarity index 50%
rename from src/Schema/Concerns/Tables.php
rename to src/Schema/Concerns/TableCommands.php
index dfecfd0..8212efd 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.
@@ -17,35 +17,55 @@ trait Tables
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/src/Schema/Grammar.php b/src/Schema/Grammar.php
index 9f31fb1..7bbf97a 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
@@ -19,6 +20,55 @@ 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|null $schema
+ * @param string $table
+ * @return Fluent
+ * @throws BindException
+ */
+ 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,
+ ];
+
+ $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,
+ type: 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/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/Concerns/InteractsWithDatabase.php b/src/Testing/Concerns/InteractsWithDatabase.php
index f73e651..a0f9195 100644
--- a/src/Testing/Concerns/InteractsWithDatabase.php
+++ b/src/Testing/Concerns/InteractsWithDatabase.php
@@ -5,133 +5,22 @@
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.
* 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
+ *
+ * @SuppressWarnings("PHPMD.UnusedFormalParameter")
*/
- public function castAsJson($value)
+ public function castAsJson($value, $connection = null)
{
return DB::raw($value);
}
-
}
diff --git a/src/Testing/DatabaseMigrations.php b/src/Testing/DatabaseMigrations.php
index ea3363b..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/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/DatabaseTruncation.php b/src/Testing/DatabaseTruncation.php
index 04bb172..272de56 100644
--- a/src/Testing/DatabaseTruncation.php
+++ b/src/Testing/DatabaseTruncation.php
@@ -4,15 +4,21 @@
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
{
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 +27,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(),
@@ -32,22 +41,51 @@ protected function migrateFreshUsing()
}
/**
- * Determine if types should be dropped when refreshing the database.
+ * 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);
+ }
+
+ /**
+ * Truncate the database tables for the given database connection.
*
- * @return array|string>
+ * @param \Illuminate\Database\ConnectionInterface $connection
+ * @param string|null $name
+ * @return void
*/
- protected function setMigrationPaths()
+ protected function truncateTablesForConnection(ConnectionInterface $connection, ?string $name): void
{
- $migrationSettings = [];
+ $dispatcher = $connection->getEventDispatcher();
- if(property_exists($this, 'realPath')) {
- $migrationSettings['--realpath'] = $this->realPath ?? false;
- }
+ $connection->unsetEventDispatcher();
- if (property_exists($this, 'migrationPaths')) {
- $migrationSettings['--path'] = $this->migrationPaths;
- }
+ (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();
+ }
+ });
+ });
- return $migrationSettings;
+ $connection->setEventDispatcher($dispatcher);
}
+
}
diff --git a/src/Testing/RefreshDatabase.php b/src/Testing/RefreshDatabase.php
index 1cfece4..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;
/**
@@ -22,7 +24,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);
@@ -49,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()
@@ -60,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(),
@@ -79,7 +89,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 8a8809f..0b0085e 100644
--- a/src/helpers.php
+++ b/src/helpers.php
@@ -46,6 +46,17 @@ function isDotString(string $string): bool
}
}
+if (!function_exists('mapObjectToArray')) {
+ function mapObjectToArray(mixed $value): mixed
+ {
+ if (!is_object($value) && !is_array($value)) {
+ return $value;
+ }
+
+ return array_map('mapObjectToArray', (array) $value);
+ }
+}
+
if (!function_exists('renameArrayKey')) {
/**
* @param array $array
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/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/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/Console/MigrateFreshCommandTest.php b/tests/Console/MigrateFreshCommandTest.php
index 52aa689..0e71f5a 100644
--- a/tests/Console/MigrateFreshCommandTest.php
+++ b/tests/Console/MigrateFreshCommandTest.php
@@ -1,5 +1,7 @@
assertExitCode(0);
$collections = $this->schemaManager->getCollections(true);
- expect(count($collections))->toBe(15);
+ expect(count($collections))->toBe($this->tableCount);
});
test('migrate:fresh --database=arangodb', function () {
@@ -46,7 +48,7 @@
])->assertExitCode(0);
$collections = $this->schemaManager->getCollections(true);
- expect(count($collections))->toBe(15);
+ expect(count($collections))->toBe($this->tableCount);
});
test('migrate:fresh --database=none', function () {
@@ -54,3 +56,180 @@
'--database' => '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 () {
+ $initialAnalyzers = $this->schemaManager->getAnalyzers();
+
+ $path = [
+ realpath(__DIR__ . '/../../TestSetup/Database/Migrations'),
+ ];
+
+ if (!$this->schemaManager->hasAnalyzer('dropMyAnalyzer')) {
+ Schema::createAnalyzer(
+ 'dropMyAnalyzer',
+ 'identity',
+ );
+ }
+
+
+ $analyzersAfterCreation = $this->schemaManager->getAnalyzers();
+
+ $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);
+
+ $endAnalyzers = $this->schemaManager->getAnalyzers();
+
+ expect(count($analyzersAfterCreation))->toBe(1 + count($initialAnalyzers));
+ expect(count($initialAnalyzers))->toBe(count($endAnalyzers));
+});
+
+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 () {
+ $initialViews = $this->schemaManager->getViews();
+ $initialAnalyzers = $this->schemaManager->getAnalyzers();
+ $initialGraphs = $this->schemaManager->getGraphs();
+
+ $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);
+
+ $endAnalyzers = $this->schemaManager->getAnalyzers();
+ $endGraphs = $this->schemaManager->getGraphs();
+ $endViews = $this->schemaManager->getViews();
+
+ expect(count($initialAnalyzers))->toBe(count($endAnalyzers));
+ expect(count($initialGraphs))->toBe(count($endGraphs));
+ expect(count($initialViews))->toBe(count($endViews));
+
+});
+
+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/MigrateInstallCommandTest.php b/tests/Console/MigrateInstallCommandTest.php
index a483ddf..c7a832a 100644
--- a/tests/Console/MigrateInstallCommandTest.php
+++ b/tests/Console/MigrateInstallCommandTest.php
@@ -34,4 +34,4 @@
test('migrate:install --database=none', function () {
$this->artisan('migrate:install', ['--database' => 'none'])->assertExitCode(0);
-})->throws(ErrorException::class);
+})->throws(InvalidArgumentException::class);
diff --git a/tests/Console/MigrateRefreshCommandTest.php b/tests/Console/MigrateRefreshCommandTest.php
index a61fda2..eca1d1c 100644
--- a/tests/Console/MigrateRefreshCommandTest.php
+++ b/tests/Console/MigrateRefreshCommandTest.php
@@ -24,7 +24,7 @@
])->assertExitCode(0);
$collections = $this->schemaManager->getCollections(true);
- expect(count($collections))->toBe(15);
+ expect(count($collections))->toBe($this->tableCount);
});
test('migrate:refresh --database=arangodb', function () {
@@ -45,7 +45,7 @@
])->assertExitCode(0);
$collections = $this->schemaManager->getCollections(true);
- expect(count($collections))->toBe(15);
+ expect(count($collections))->toBe($this->tableCount);
});
test('migrate:refresh --database=none', function () {
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/Console/ShowModelCommandTest.php b/tests/Console/ShowModelCommandTest.php
new file mode 100644
index 0000000..c1e2e28
--- /dev/null
+++ b/tests/Console/ShowModelCommandTest.php
@@ -0,0 +1,44 @@
+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();
+});
+
+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();
+});
diff --git a/tests/Console/WipeCommandTest.php b/tests/Console/WipeCommandTest.php
new file mode 100644
index 0000000..49a398d
--- /dev/null
+++ b/tests/Console/WipeCommandTest.php
@@ -0,0 +1,158 @@
+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 () {
+ $initialAnalyzers = $this->schemaManager->getAnalyzers();
+
+ $schemaManager = $this->connection->getArangoClient()->schema();
+ if (!$schemaManager->hasAnalyzer('dropMyAnalyzer')) {
+ Schema::createAnalyzer(
+ 'dropMyAnalyzer',
+ 'identity',
+ );
+ }
+
+ $this->artisan('db:wipe', [
+ '--drop-analyzers' => true,
+ ])->assertExitCode(0);
+
+ $endAnalyzers = $this->schemaManager->getAnalyzers();
+ expect(count($initialAnalyzers))->toBe(count($endAnalyzers));
+});
+
+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 () {
+ $initialAnalyzers = $this->schemaManager->getAnalyzers();
+ $initialViews = $this->schemaManager->getViews();
+ $initialGraphs = $this->schemaManager->getGraphs();
+
+ $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);
+
+ $endAnalyzers = $this->schemaManager->getAnalyzers();
+ $endGraphs = $this->schemaManager->getGraphs();
+ $endViews = $this->schemaManager->getViews();
+
+ 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 () {
+ $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 bf85cd2..a5dab05 100644
--- a/tests/Database/ConnectionTest.php
+++ b/tests/Database/ConnectionTest.php
@@ -122,3 +122,17 @@
//
// Schema::hasTable('dummy');
//})->throws(QueryException::class);
+
+test('threadCount', function () {
+ $connection = $this->connection;
+
+ 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');
+});
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/CastAttributesTest.php b/tests/Eloquent/CastAttributesTest.php
index 047bd40..74cbc51 100644
--- a/tests/Eloquent/CastAttributesTest.php
+++ b/tests/Eloquent/CastAttributesTest.php
@@ -30,6 +30,12 @@
$profile = [
'firstName' => fake()->firstName,
'lastName' => fake()->lastName,
+ 'address' => [
+ 'streetAddress' => fake()->streetAddress,
+ 'city' => fake()->city,
+ 'postcode' => fake()->postcode,
+ 'country' => fake()->country,
+ ],
];
$user = User::create([
@@ -43,13 +49,22 @@
expect($user->profileAsArray)->toBeArray();
expect($refreshedUser->profileAsArray)->toBeArray();
+ expect($refreshedUser->profileAsArray['address'])->toBeArray();
+
expect($retrievedUser->profileAsArray)->toBeObject();
+ expect($retrievedUser->profileAsArray->address)->toBeObject();
});
test('Cast attribute to ArrayObject', function () {
$profile = [
'firstName' => fake()->firstName,
'lastName' => fake()->lastName,
+ 'address' => [
+ 'streetAddress' => fake()->streetAddress,
+ 'city' => fake()->city,
+ 'postcode' => fake()->postcode,
+ 'country' => fake()->country,
+ ],
];
$user = User::create([
@@ -66,8 +81,11 @@
expect($user->profileAsArrayObjectCast)->toBeInstanceOf(ArrayObject::class);
expect($refreshedUser->profileAsArrayObjectCast)->toBeInstanceOf(ArrayObject::class);
+ expect($refreshedUser->profileAsArrayObjectCast->address)->toBeArray();
+
expect($refreshedUser->profileAsArrayObjectCast->age)->toBe(24);
expect($retrievedUser->profileAsArrayObjectCast)->toBeObject();
+ expect($retrievedUser->profileAsArrayObjectCast->address)->toBeObject();
});
test('Cast attribute to object', function () {
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/Eloquent/ModelTest.php b/tests/Eloquent/ModelTest.php
index 2d72a57..05cd79f 100644
--- a/tests/Eloquent/ModelTest.php
+++ b/tests/Eloquent/ModelTest.php
@@ -2,6 +2,8 @@
use LaravelFreelancerNL\Aranguent\Testing\DatabaseTransactions;
use TestSetup\Models\Character;
+use TestSetup\Models\User;
+use LaravelFreelancerNL\Aranguent\Exceptions\QueryException;
uses(
DatabaseTransactions::class,
@@ -18,21 +20,49 @@
expect($fresh->age)->toBe(($initialAge + 1));
});
-test('update or create', function () {
- $character = Character::first();
- $initialAge = $character->age;
- $newAge = ($initialAge + 1);
+test('updateOrCreate', function () {
+ $userData = [
+ "username" => "Dunk",
+ "email" => "d.the.tall@hedgeknight.com",
+ ];
- $character->updateOrCreate(['age' => $initialAge], ['age' => $newAge]);
+ $user1 = User::updateOrCreate($userData);
- $fresh = $character->fresh();
+ $user2 = User::where("email", "d.the.tall@hedgeknight.com")->first();
- expect($fresh->age)->toBe($newAge);
+ expect($user1->_id)->toBe($user2->_id);
});
-test('upsert', function () {
- $this->skipTestOnArangoVersionsBefore('3.7');
+test('updateOrCreate runs twice', function () {
+ $userData = [
+ "username" => "Dunk",
+ "email" => "d.the.tall@hedgeknight.com",
+ ];
+ $user1 = User::updateOrCreate($userData);
+
+ $user2 = User::updateOrCreate($userData);
+
+ expect($user1->_id)->toBe($user2->_id);
+});
+
+test('updateOrCreate throws error on unique key if data is different', function () {
+ $userData = [
+ "username" => "Dunk",
+ "email" => "d.the.tall@hedgeknight.com",
+ ];
+ $user1 = User::updateOrCreate($userData);
+
+ $userData = [
+ "username" => "Duncan the Tall",
+ "email" => "d.the.tall@hedgeknight.com",
+ ];
+ $user2 = User::updateOrCreate($userData);
+
+ expect($user1->_id)->toBe($user2->_id);
+})->throws(QueryException::class);
+
+test('upsert', function () {
Character::upsert(
[
[
@@ -63,6 +93,28 @@
expect($jaime->alive)->toBeFalse();
});
+test('upsert runs twice', function () {
+ $userData = [
+ "username" => "Dunk",
+ "email" => "d.the.tall@hedgeknight.com",
+ ];
+
+ User::upsert([$userData], ['email'], ['username']);
+
+ $userData = [
+ "username" => "Duncan the Tall",
+ "email" => "d.the.tall@hedgeknight.com",
+ ];
+
+ $result = User::upsert([$userData], ['email'], ['username']);
+
+ $user = User::where("email", "d.the.tall@hedgeknight.com")->first();
+
+ expect($result)->toBe(1);
+ expect($user->username)->toBe("Duncan the Tall");
+});
+
+
test('delete model', function () {
$character = Character::first();
@@ -131,3 +183,63 @@
expect($ned->id)->toEqual('NedStarkIsDead');
expect($ned->_id)->toEqual('characters/NedStarkIsDead');
});
+
+test('firstOrCreate', function () {
+ $userData = [
+ "username" => "Dunk",
+ "email" => "d.the.tall@hedgeknight.com",
+ ];
+
+ $user = User::firstOrCreate($userData);
+
+ $result = DB::table('users')
+ ->where('email', 'd.the.tall@hedgeknight.com')
+ ->first();
+
+ expect($result->_id)->toBe($user->_id);
+});
+
+test('firstOrCreate runs twice without error', function () {
+ $userData = [
+ "username" => "Dunk",
+ "email" => "d.the.tall@hedgeknight.com",
+ ];
+
+ $user1 = User::firstOrCreate($userData);
+ $user2 = User::firstOrCreate($userData);
+
+ expect($user1->_id)->toBe($user2->_id);
+});
+
+test('firstOrCreate throws error if data is different', function () {
+ $userData = [
+ "username" => "Dunk",
+ "email" => "d.the.tall@hedgeknight.com",
+ ];
+
+ $user1 = User::firstOrCreate($userData);
+
+ $userData = [
+ "username" => "Duncan the Tall",
+ "email" => "d.the.tall@hedgeknight.com",
+ ];
+ $user2 = User::firstOrCreate($userData);
+
+ expect($user1->_id)->toBe($user2->_id);
+})->throws(QueryException::class);
+
+
+test('createOrFirst', function () {
+ $userData = [
+ "username" => "Dunk",
+ "email" => "d.the.tall@hedgeknight.com",
+ ];
+
+ $user = User::createOrFirst($userData);
+
+ $result = DB::table('users')
+ ->where('email', 'd.the.tall@hedgeknight.com')
+ ->first();
+
+ expect($result->_id)->toBe($user->_id);
+});
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 () {
diff --git a/tests/Migrations/MigrationRepositoryTest.php b/tests/Migrations/MigrationRepositoryTest.php
index 5818911..d2f0fa0 100644
--- a/tests/Migrations/MigrationRepositoryTest.php
+++ b/tests/Migrations/MigrationRepositoryTest.php
@@ -10,8 +10,8 @@
});
afterEach(function () {
- $migrations = $this->databaseMigrationRepository->getMigrations(11);
- foreach($migrations as $migration) {
+ $migrations = $this->databaseMigrationRepository->getMigrations(12);
+ foreach ($migrations as $migration) {
$this->databaseMigrationRepository->delete($migration);
}
});
@@ -42,7 +42,7 @@
$this->databaseMigrationRepository->delete($migration);
$results = $this->databaseMigrationRepository->getRan();
- ray('delete getRan', $results);
+
expect($results)->toBeEmpty();
});
@@ -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/Pest.php b/tests/Pest.php
index f76a4c4..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);
@@ -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/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/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 23fa65a..fe6be9f 100644
--- a/tests/Query/InsertTest.php
+++ b/tests/Query/InsertTest.php
@@ -25,14 +25,14 @@
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']);
expect($result)->toEqual(1);
});
-test('insert or ignore inserts data', function () {
+test('insertOrIgnore inserts data', function () {
$characterData = [
"_key" => "LyannaStark",
"name" => "Lyanna",
@@ -58,7 +58,7 @@
expect($result->count())->toBe(1);
});
-test('insert or ignore doesnt error on duplicates', function () {
+test('insertOrIgnore doesnt error on duplicates', function () {
$characterData = [
"_key" => "LyannaStark",
"name" => "Lyanna",
@@ -78,6 +78,31 @@
expect($result->count())->toBe(1);
});
+test('insertOrIgnore with unique index on non-primary fields', function () {
+ $userData = [
+ "_key" => "LyannaStark",
+ "username" => "Lyanna Stark",
+ "email" => "l.stark@windsofwinter.com",
+ ];
+ DB::table('users')->insertOrIgnore($userData);
+
+ $result = DB::table('users')
+ ->first();
+
+ expect($result->_id)->toBe('users/LyannaStark');
+
+ $userData = [
+ "username" => "Lya Stark",
+ "email" => "l.stark@windsofwinter.com",
+ ];
+ DB::table('users')->insertOrIgnore($userData);
+
+ $result = DB::table('users')
+ ->first();
+
+ expect($result->_id)->toBe('users/LyannaStark');
+});
+
test('insert embedded empty array', function () {
$characterData = [
"_key" => "LyannaStark",
@@ -100,14 +125,41 @@
expect($result->first()->tags)->toBeEmpty();
});
-test('insert using', function () {
+test('insertUsing', function () {
// Let's give Baelish a user, what could possibly go wrong?
$baelishes = DB::table('characters')
->where('surname', 'Baelish');
DB::table('users')->insertUsing(['name', 'surname'], $baelishes);
- $user = DB::table('users')->first();
+ $user = DB::table('users')->where("surname", "=", "Baelish")->first();
+
+ 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');
});
diff --git a/tests/Query/JoinTest.php b/tests/Query/JoinTest.php
index 0294cfc..cc6f96d 100644
--- a/tests/Query/JoinTest.php
+++ b/tests/Query/JoinTest.php
@@ -48,7 +48,7 @@
$characters = $builder->get();
expect($characters)->toHaveCount(7);
- expect($characters[0]->age)->toEqual(16);
+ expect($characters[0]->age)->toEqual($this->tableCount);
expect($characters[0]->surname)->toEqual('Targaryen');
});
@@ -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');
+});
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/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 () {
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);
+});
diff --git a/tests/Query/UpdateTest.php b/tests/Query/UpdateTest.php
index 0e9fde6..d1692d1 100644
--- a/tests/Query/UpdateTest.php
+++ b/tests/Query/UpdateTest.php
@@ -67,6 +67,22 @@
->and($result->en->words)->toBe($words);
});
+test('updateOrInsert without values', function () {
+ $exists = DB::table('characters')->where('id', 'JaimeLannister')->exists();
+ expect($exists)->toBeTrue();
+
+ $result = DB::table('characters')
+ ->updateOrInsert(["_key" => "JaimeLannister"], []);
+
+ $exists = DB::table('characters')->where('id', 'JaimeLannister')->exists();
+
+ expect($result)->toBeTrue();
+ expect($exists)->toBeTrue();
+});
+
+
+
+
test('updateOrInsert updates', function () {
DB::table('characters')
->updateOrInsert(["_key" => "NedStark"], ["alive" => false, "age" => 42]);
@@ -96,6 +112,42 @@
expect($result)->toBeTrue();
});
+test('updateOrInsert inserts with callback', function () {
+ $result = DB::table('characters')->where('id', 'LyannaStark')->exists();
+
+ expect($result)->toBeFalse();
+
+ $data = [
+ "name" => "Lyanna",
+ "surname" => "Stark",
+ "alive" => false,
+ "age" => 25,
+ "residence_id" => "winterfell",
+ "tags" => [],
+ ];
+
+ DB::table('characters')->updateOrInsert(
+ ["_key" => "LyannaStark"],
+ function ($exists) use ($data) {
+ if ($exists) {
+ return [
+ "name" => "Lyanna",
+ "surname" => "Stark",
+ ];
+ }
+
+ return [
+ "name" => "Lyanna",
+ "surname" => "Stark",
+ "alive" => $data['alive'],
+ ];
+ },
+ );
+
+ $result = DB::table('characters')->where('id', 'LyannaStark')->exists();
+
+ expect($result)->toBeTrue();
+});
test('increment', function () {
$youngNed = DB::table('characters')->where('id', 'NedStark')->first();
diff --git a/tests/Query/WheresTest.php b/tests/Query/WheresTest.php
index 7f85a4e..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')
@@ -67,8 +67,23 @@
);
});
+test('where =~ operator', function () {
+ $builder = getBuilder($this->connection);
+ $builder->select('*')
+ ->from('users')
+ ->where('email', '=~', 'email@example.com');
+
+ $this->assertSame(
+ 'FOR userDoc IN users '
+ . 'FILTER `userDoc`.`email` =~ @'
+ . $builder->getQueryId()
+ . '_where_1 RETURN userDoc',
+ $builder->toSql(),
+ );
+});
+
test('where json arrow conversion', function () {
- $builder = getBuilder();
+ $builder = getBuilder($this->connection);
$builder->select('*')
->from('users')
->where('email->address', '=', 'email@example.com')
@@ -86,7 +101,7 @@
});
test('where json contains', function () {
- $builder = getBuilder();
+ $builder = getBuilder($this->connection);
$builder->select('*')
->from('users')
->whereJsonContains('options->languages', 'en');
@@ -101,7 +116,7 @@
});
test('where json length', function () {
- $builder = getBuilder();
+ $builder = getBuilder($this->connection);
$builder->select('*')
->from('users')
->whereJsonLength('options->languages', '>', 'en');
@@ -116,7 +131,7 @@
});
test('where between', function () {
- $builder = getBuilder();
+ $builder = getBuilder($this->connection);
$builder->select('*')->from('users')->whereBetween('votes', [1, 100]);
$this->assertSame(
@@ -130,7 +145,7 @@
});
test('where not between', function () {
- $builder = getBuilder();
+ $builder = getBuilder($this->connection);
$builder->select('*')->from('users')->whereNotBetween('votes', [1, 100]);
$this->assertSame(
@@ -144,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(
@@ -155,7 +170,7 @@
});
test('where column', function () {
- $builder = getBuilder();
+ $builder = getBuilder($this->connection);
$builder->select('*')->from('users')->whereColumn('first_name', '=', 'last_name');
$this->assertSame(
@@ -165,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(
@@ -175,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)
@@ -195,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)
@@ -214,23 +229,32 @@
);
});
-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();
- $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(),
- );
+ expect($results)->toHaveCount(33);
+ expect($results->first()->_id)->toBe('characters/NedStark');
+ expect($results->first()->residence_id)->toBe('winterfell');
});
+
+
test('where integer in raw', function () {
- $builder = getBuilder();
+ $builder = getBuilder($this->connection);
$builder->select()
->from('users')
@@ -243,7 +267,7 @@
});
test('where not in', function () {
- $builder = getBuilder();
+ $builder = getBuilder($this->connection);
$builder->select()
->from('users')
@@ -258,7 +282,7 @@
});
test('where integer not in raw', function () {
- $builder = getBuilder();
+ $builder = getBuilder($this->connection);
$builder->select()
->from('users')
@@ -271,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(
@@ -283,7 +307,7 @@
});
test('where year', function () {
- $builder = getBuilder();
+ $builder = getBuilder($this->connection);
$builder->select('*')->from('users')->whereYear('created_at', '2016');
$this->assertSame(
@@ -295,7 +319,7 @@
});
test('where month', function () {
- $builder = getBuilder();
+ $builder = getBuilder($this->connection);
$builder->select('*')->from('users')->whereMonth('created_at', '12');
$this->assertSame(
@@ -307,7 +331,7 @@
});
test('where day', function () {
- $builder = getBuilder();
+ $builder = getBuilder($this->connection);
$builder->select('*')->from('users')->whereDay('created_at', '31');
$this->assertSame(
@@ -319,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(
@@ -403,7 +427,7 @@
});
test('where nested', function () {
- $builder = getBuilder();
+ $builder = getBuilder($this->connection);
$query = $builder->select('*')
->from('characters')
@@ -506,3 +530,208 @@
expect($results->count())->toBe(2);
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($this->connection);
+ $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($this->connection);
+ $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($this->connection);
+ $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();
+
+ 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);
+});
+
+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');
+});
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/AnalyzerTest.php b/tests/Schema/AnalyzerTest.php
index f10a568..d96149c 100644
--- a/tests/Schema/AnalyzerTest.php
+++ b/tests/Schema/AnalyzerTest.php
@@ -7,9 +7,10 @@
test('createAnalyzer', function () {
$schemaManager = $this->connection->getArangoClient()->schema();
if (!$schemaManager->hasAnalyzer('myAnalyzer')) {
- Schema::createAnalyzer('myAnalyzer', [
- 'type' => 'identity',
- ]);
+ Schema::createAnalyzer(
+ 'myAnalyzer',
+ 'identity',
+ );
}
$analyzer = $schemaManager->getAnalyzer('myAnalyzer');
@@ -18,25 +19,23 @@
$schemaManager->deleteAnalyzer('myAnalyzer');
});
-test('getAllAnalyzers', function () {
+test('getAnalyzers', function () {
$schemaManager = $this->connection->getArangoClient()->schema();
+ $initialAnalyzers = $schemaManager->getAnalyzers();
- $analyzers = Schema::getAllAnalyzers();
+ $analyzers = Schema::getAnalyzers();
- expect($analyzers)->toHaveCount(13);
+ $endAnalyzers = $schemaManager->getAnalyzers();
+ expect(count($initialAnalyzers))->toBe(count($endAnalyzers));
});
test('replaceAnalyzer', function () {
$schemaManager = $this->connection->getArangoClient()->schema();
if (!$schemaManager->hasAnalyzer('myAnalyzer')) {
- Schema::createAnalyzer('myAnalyzer', [
- 'type' => 'identity',
- ]);
+ Schema::createAnalyzer('myAnalyzer', 'identity');
}
- Schema::replaceAnalyzer('myAnalyzer', [
- 'type' => 'identity',
- ]);
+ Schema::replaceAnalyzer('myAnalyzer', 'identity');
$schemaManager->deleteAnalyzer('myAnalyzer');
});
@@ -44,9 +43,7 @@
test('dropAnalyzer', function () {
$schemaManager = $this->connection->getArangoClient()->schema();
if (!$schemaManager->hasAnalyzer('myAnalyzer')) {
- Schema::createAnalyzer('myAnalyzer', [
- 'type' => 'identity',
- ]);
+ Schema::createAnalyzer('myAnalyzer', 'identity');
}
Schema::dropAnalyzer('myAnalyzer');
@@ -56,9 +53,7 @@
test('dropAnalyzerIfExists true', function () {
$schemaManager = $this->connection->getArangoClient()->schema();
if (!$schemaManager->hasAnalyzer('myAnalyzer')) {
- Schema::createAnalyzer('myAnalyzer', [
- 'type' => 'identity',
- ]);
+ Schema::createAnalyzer('myAnalyzer', 'identity');
}
Schema::dropAnalyzerIfExists('myAnalyzer');
@@ -70,3 +65,21 @@
Schema::dropAnalyzerIfExists('none-existing-analyzer');
});
+
+test('dropAllAnalyzers', function () {
+ $schemaManager = $this->connection->getArangoClient()->schema();
+
+ $initialAnalyzers = Schema::getAnalyzers();
+
+ Schema::createAnalyzer('myAnalyzer1', 'identity');
+ Schema::createAnalyzer('myAnalyzer2', 'identity');
+
+ $totalAnalyzers = Schema::getAnalyzers();
+
+ Schema::dropAllAnalyzers();
+
+ $endAnalyzers = Schema::getAnalyzers();
+
+ expect(count($initialAnalyzers))->toBe(count($endAnalyzers));
+ expect(count($initialAnalyzers))->toBe(count($totalAnalyzers) - 2);
+});
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/GraphTest.php b/tests/Schema/GraphTest.php
new file mode 100644
index 0000000..9358f00
--- /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('getGraphs', function () {
+ $schemaManager = $this->connection->getArangoClient()->schema();
+
+ $graphs = Schema::getGraphs();
+ expect($graphs)->toHaveCount(0);
+
+ if (!$schemaManager->hasGraph('myGraph')) {
+ createGraph();
+ }
+
+ $graphs = Schema::getGraphs();
+ 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::getGraphs();
+
+ Schema::createGraph('myGraph1');
+ Schema::createGraph('myGraph2');
+
+ $totalGraphs = Schema::getGraphs();
+
+ Schema::dropAllGraphs();
+
+ $endGraphs = Schema::getGraphs();
+
+ expect(count($initialGraphs))->toBe(count($endGraphs));
+ expect(count($initialGraphs))->toBe(count($totalGraphs) - 2);
+});
diff --git a/tests/Schema/IndexTest.php b/tests/Schema/IndexTest.php
index 2ff8b7f..843b04e 100644
--- a/tests/Schema/IndexTest.php
+++ b/tests/Schema/IndexTest.php
@@ -33,6 +33,26 @@
expect($searchResult)->toBeFalse();
});
+
+test('Schema::hasIndex', function () {
+ Schema::table('characters', function (Blueprint $table) {
+ $table->index(['name']);
+ });
+ $indexName = 'characters_name_persistent';
+
+ expect(Schema::hasIndex('characters', $indexName))->toBeTrue();
+ expect(Schema::hasIndex('characters', 'notAnIndex'))->toBeFalse();
+});
+
+test('Schema::hasIndex by columns', function () {
+ Schema::table('characters', function (Blueprint $table) {
+ $table->index(['name']);
+ });
+
+ expect(Schema::hasIndex('characters', ['name']))->toBeTrue();
+ expect(Schema::hasIndex('characters', ['name', 'lastName']))->toBeFalse();
+});
+
test('index names only contains alpha numeric characters', function () {
Schema::table('characters', function (Blueprint $table) {
$indexName = $table->createIndexName('persistent', ['addresses[*]']);
@@ -97,6 +117,98 @@
});
});
+test('invertedIndex with field properties', function () {
+ Schema::table('characters', function (Blueprint $table) {
+ $table->invertedIndex(
+ [
+ 'name',
+ [
+ 'name' => 'surname',
+ 'analyzer' => "text_en",
+ 'searchField' => true,
+ 'includeAllFields' => true,
+ ],
+ 'age',
+ ],
+ );
+ });
+
+ $expectedName = 'characters_name_surname_age_inverted';
+
+ $index = $this->schemaManager->getIndexByName('characters', $expectedName);
+
+ expect($index->fields)->tobeArray();
+ expect($index->fields)->toHaveCount(3);
+
+ expect($index->fields[1]->analyzer)->toBe('text_en');
+
+ Schema::table('characters', function (Blueprint $table) use ($index) {
+ $table->dropInvertedIndex($index->name);
+ });
+});
+
+test('multiDimensionalIndex && dropMultiDimensionalIndex', function () {
+ $this->skipTestOn('arangodb', '<', '3.12');
+
+ $name = 'events_timeline_mdi';
+
+ Schema::table('events', function (Blueprint $table) use ($name) {
+ $table->multiDimensionalIndex(
+ columns: [
+ 'timeline.starts_at',
+ 'timeline.ends_at',
+ ],
+ name: $name,
+ indexOptions: [
+ 'fieldValueTypes' => 'double',
+ ],
+ );
+ });
+
+ $index = $this->schemaManager->getIndexByName('events', $name);
+
+ expect($index->name)->toEqual($name);
+ expect($index->type)->toEqual('mdi');
+
+ Schema::table('events', function (Blueprint $table) use ($name) {
+ $table->dropMultiDimensionalIndex($name);
+ });
+});
+
+test('Prefixed multiDimensionalIndex', function () {
+ $this->skipTestOn('arangodb', '<', '3.12');
+
+ $name = 'events_timeline_mdi_prefixed';
+
+ Schema::table('events', function (Blueprint $table) use ($name) {
+ $table->multiDimensionalIndex(
+ columns: [
+ 'timeline.starts_at',
+ 'timeline.ends_at',
+ ],
+ name: $name,
+ indexOptions: [
+ 'fieldValueTypes' => 'double',
+ 'prefixFields' => [
+ 'age',
+ 'type',
+ ],
+ ],
+ type: 'mdi-prefixed',
+ );
+ });
+
+ $index = $this->schemaManager->getIndexByName('events', $name);
+
+ expect($index->name)->toEqual($name);
+ expect($index->type)->toEqual('mdi-prefixed');
+
+ Schema::table('events', function (Blueprint $table) use ($name) {
+ $table->dropMultiDimensionalIndex($name);
+ });
+});
+
+
test('persistentIndex', function () {
Schema::table('characters', function (Blueprint $table) {
$table->persistentIndex(['name']);
diff --git a/tests/Schema/SchemaBuilderTest.php b/tests/Schema/SchemaBuilderTest.php
index 12796f8..2ba9a01 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);
@@ -45,12 +45,12 @@
});
test('drop all tables', function () {
- $initialTables = Schema::getAllTables();
+ $initialTables = Schema::getTables();
Schema::dropAllTables();
- $tables = Schema::getAllTables();
+ $tables = Schema::getTables();
- expect(count($initialTables))->toEqual(15);
+ expect(count($initialTables))->toEqual($this->tableCount);
expect(count($tables))->toEqual(0);
$this->artisan('migrate:install')->assertExitCode(0);
@@ -85,7 +85,7 @@
}
$view = Schema::getView('search');
- expect($view->name)->toEqual('search');
+ expect($view['name'])->toEqual('search');
$schemaManager->deleteView('search');
});
@@ -102,14 +102,14 @@
Schema::createView('search', []);
}
- $views = Schema::getAllViews();
+ $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');
@@ -217,9 +217,7 @@
test('createAnalyzer', function () {
$schemaManager = $this->connection->getArangoClient()->schema();
if (!$schemaManager->hasAnalyzer('myAnalyzer')) {
- Schema::createAnalyzer('myAnalyzer', [
- 'type' => 'identity',
- ]);
+ Schema::createAnalyzer('myAnalyzer', 'identity');
}
$analyzer = $schemaManager->getAnalyzer('myAnalyzer');
@@ -228,25 +226,22 @@
$schemaManager->deleteAnalyzer('myAnalyzer');
});
-test('getAllAnalyzers', function () {
+test('getAnalyzers', function () {
$schemaManager = $this->connection->getArangoClient()->schema();
+ $initialAnalyzers = $schemaManager->getAnalyzers();
- $analyzers = Schema::getAllAnalyzers();
+ $analyzers = Schema::getAnalyzers();
- expect($analyzers)->toHaveCount(13);
+ expect(count($initialAnalyzers))->toBe(count($analyzers));
});
test('replaceAnalyzer', function () {
$schemaManager = $this->connection->getArangoClient()->schema();
if (!$schemaManager->hasAnalyzer('myAnalyzer')) {
- Schema::createAnalyzer('myAnalyzer', [
- 'type' => 'identity',
- ]);
+ Schema::createAnalyzer('myAnalyzer', 'identity');
}
- Schema::replaceAnalyzer('myAnalyzer', [
- 'type' => 'identity',
- ]);
+ Schema::replaceAnalyzer('myAnalyzer', 'identity');
$schemaManager->deleteAnalyzer('myAnalyzer');
});
@@ -254,9 +249,7 @@
test('dropAnalyzer', function () {
$schemaManager = $this->connection->getArangoClient()->schema();
if (!$schemaManager->hasAnalyzer('myAnalyzer')) {
- Schema::createAnalyzer('myAnalyzer', [
- 'type' => 'identity',
- ]);
+ Schema::createAnalyzer('myAnalyzer', 'identity');
}
Schema::dropAnalyzer('myAnalyzer');
@@ -266,9 +259,7 @@
test('dropAnalyzerIfExists true', function () {
$schemaManager = $this->connection->getArangoClient()->schema();
if (!$schemaManager->hasAnalyzer('myAnalyzer')) {
- Schema::createAnalyzer('myAnalyzer', [
- 'type' => 'identity',
- ]);
+ Schema::createAnalyzer('myAnalyzer', 'identity');
}
Schema::dropAnalyzerIfExists('myAnalyzer');
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 694db2e..d01b32b 100644
--- a/tests/Schema/TableTest.php
+++ b/tests/Schema/TableTest.php
@@ -1,72 +1,20 @@
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();
@@ -95,13 +43,13 @@
});
test('dropAllTables', function () {
- $initialTables = Schema::getAllTables();
+ $initialTables = Schema::getTables();
Schema::dropAllTables();
- $tables = Schema::getAllTables();
+ $tables = Schema::getTables();
- expect(count($initialTables))->toEqual(15);
+ expect(count($initialTables))->toEqual($this->tableCount);
expect(count($tables))->toEqual(0);
refreshDatabase();
diff --git a/tests/Schema/ViewTest.php b/tests/Schema/ViewTest.php
index 806fe80..8cdce64 100644
--- a/tests/Schema/ViewTest.php
+++ b/tests/Schema/ViewTest.php
@@ -23,12 +23,12 @@
}
$view = Schema::getView('search');
- expect($view->name)->toEqual('search');
+ expect($view['name'])->toEqual('search');
$schemaManager->deleteView('search');
});
-test('getAllViews', function () {
+test('getViews', function () {
$schemaManager = $this->connection->getArangoClient()->schema();
if (!$schemaManager->hasView('pages')) {
Schema::createView('pages', []);
@@ -40,14 +40,14 @@
Schema::createView('search', []);
}
- $views = Schema::getAllViews();
+ $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/TestCase.php b/tests/TestCase.php
index 25219c2..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;
@@ -36,6 +36,8 @@ class TestCase extends \Orchestra\Testbench\TestCase
protected string $seeder = DatabaseSeeder::class;
+ protected int $tableCount = 16;
+
/**
* The base URL to use while testing the application.
*
@@ -206,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);
}
}
@@ -225,6 +227,10 @@ protected function skipTestOn(string $software, string $operator = '<', string $
$currentVersion = getenv(strtoupper($software . '_VERSION'));
}
+ if (!$currentVersion) {
+ return;
+ }
+
if (version_compare($currentVersion, $version, $operator)) {
$this->markTestSkipped('This test does not support ' . ucfirst($software) . ' versions ' . $operator . ' ' . $version);
}
diff --git a/tests/Testing/DatabaseTruncationTest.php b/tests/Testing/DatabaseTruncationTest.php
index 1de61c3..de1cf2d 100644
--- a/tests/Testing/DatabaseTruncationTest.php
+++ b/tests/Testing/DatabaseTruncationTest.php
@@ -9,12 +9,11 @@
uses(DatabaseTruncation::class);
test('Ensure all tables are present', function () {
- $tables = Schema::getAllTables();
+ $tables = Schema::getTables();
- expect(count($tables))->toEqual(15);
+ expect(count($tables))->toEqual($this->tableCount);
});
-
test('Ensure all characters are present', function () {
$characters = Character::all();
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);
});