Skip to content

Commit

Permalink
Added support for createOrFirst/firstOrCreate
Browse files Browse the repository at this point in the history
Added model crud tests
  • Loading branch information
LaravelFreelancerNL committed Jun 22, 2024
1 parent 7ff0c22 commit c302c4c
Show file tree
Hide file tree
Showing 10 changed files with 236 additions and 18 deletions.
1 change: 1 addition & 0 deletions TestSetup/Database/Seeders/DatabaseSeeder.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class DatabaseSeeder extends Seeder
*/
public function run()
{
$this->call(UsersSeeder::class);
$this->call(CharactersSeeder::class);
$this->call(ChildrenSeeder::class);
$this->call(LocationsSeeder::class);
Expand Down
31 changes: 31 additions & 0 deletions TestSetup/Database/Seeders/UsersSeeder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use LaravelFreelancerNL\Aranguent\Auth\User;

class UsersSeeder extends Seeder
{
/**
* Run the database Seeds.
*
* @return void
*/
public function run()
{
$users = '[
{
"_key":"LyannaStark",
"username":"Lyanna Stark",
"email":"[email protected]"
}
]';

$users = json_decode($users, JSON_OBJECT_AS_ARRAY);

foreach ($users as $user) {
User::insertOrIgnore($user);
}
}
}
1 change: 1 addition & 0 deletions TestSetup/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class User extends \LaravelFreelancerNL\Aranguent\Auth\User
protected $fillable = [
'email',
'password',
'username',
'uuid',
'is_admin',
'profileAsArray',
Expand Down
6 changes: 3 additions & 3 deletions docs/compatibility-list.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,9 @@ in the chapter above._

### Model CRUD
all / first / firstWhere / firstOr / firstOrFail /
firstOrCreate? / firstOrNew? /
firstOrCreate / firstOrNew? /
find / findOr / fresh? / refresh? /
create / fill / save / update / updateOrCreate /
create / createOrFirst / fill / save / update / updateOrCreate /
upsert / replicate / delete / destroy / truncate / softDeletes /
trashed? / restore? / withTrashed? / forceDelete
isDirty? / isClean / wasChanged / getOriginal /
Expand Down Expand Up @@ -165,8 +165,8 @@ whereDoesntHave / whereHasMorph / whereDoesntHaveMorph
#### Aggregating related models
withCount / loadCount /
withSum / loadSum / withExists / morphWithCount /loadMorphCount /

loadMorphCount

#### Eager loading
with / without / withOnly / constrain /
load / loadMissing / loadMorph / preventLazyLoading
Expand Down
21 changes: 21 additions & 0 deletions src/Concerns/RunsQueries.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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,
Expand Down
18 changes: 18 additions & 0 deletions src/Eloquent/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -20,6 +21,23 @@ 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) {
ray($e);
return $this->useWritePdo()->where($attributes)->first() ?? throw $e;
}
}

/**
* Insert a record in the database.
*
Expand Down
6 changes: 4 additions & 2 deletions src/Exceptions/QueryException.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<mixed> $bindings
* @param mixed[] $bindings
* @param \Throwable $previous
* @return string
*/
protected function formatMessage($connectionName, $sql, $bindings, Throwable $previous)
Expand Down
7 changes: 7 additions & 0 deletions src/Exceptions/UniqueConstraintViolationException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

declare(strict_types=1);

namespace LaravelFreelancerNL\Aranguent\Exceptions;

class UniqueConstraintViolationException extends QueryException {}
130 changes: 121 additions & 9 deletions tests/Eloquent/ModelTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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" => "[email protected]",
];

$character->updateOrCreate(['age' => $initialAge], ['age' => $newAge]);
$user1 = User::updateOrCreate($userData);

$fresh = $character->fresh();
$user2 = User::where("email", "[email protected]")->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" => "[email protected]",
];
$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" => "[email protected]",
];
$user1 = User::updateOrCreate($userData);

$userData = [
"username" => "Duncan the Tall",
"email" => "[email protected]",
];
$user2 = User::updateOrCreate($userData);

expect($user1->_id)->toBe($user2->_id);
})->throws(QueryException::class);


test('upsert', function () {
Character::upsert(
[
[
Expand Down Expand Up @@ -63,6 +93,28 @@
expect($jaime->alive)->toBeFalse();
});

test('upsert runs twice', function () {
$userData = [
"username" => "Dunk",
"email" => "[email protected]",
];

User::upsert([$userData], ['email'], ['username']);

$userData = [
"username" => "Duncan the Tall",
"email" => "[email protected]",
];

$result = User::upsert([$userData], ['email'], ['username']);

$user = User::where("email", "[email protected]")->first();

expect($result)->toBe(1);
expect($user->username)->toBe("Duncan the Tall");
});


test('delete model', function () {
$character = Character::first();

Expand Down Expand Up @@ -131,3 +183,63 @@
expect($ned->id)->toEqual('NedStarkIsDead');
expect($ned->_id)->toEqual('characters/NedStarkIsDead');
});

test('firstOrCreate', function () {
$userData = [
"username" => "Dunk",
"email" => "[email protected]",
];

$user = User::firstOrCreate($userData);

$result = DB::table('users')
->where('email', '[email protected]')
->first();

expect($result->_id)->toBe($user->_id);
});

test('firstOrCreate runs twice without error', function () {
$userData = [
"username" => "Dunk",
"email" => "[email protected]",
];

$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" => "[email protected]",
];

$user1 = User::firstOrCreate($userData);

$userData = [
"username" => "Duncan the Tall",
"email" => "[email protected]",
];
$user2 = User::firstOrCreate($userData);

expect($user1->_id)->toBe($user2->_id);
})->throws(QueryException::class);


test('createOrFirst', function () {
$userData = [
"username" => "Dunk",
"email" => "[email protected]",
];

$user = User::createOrFirst($userData);

$result = DB::table('users')
->where('email', '[email protected]')
->first();

expect($result->_id)->toBe($user->_id);
});
33 changes: 29 additions & 4 deletions tests/Query/InsertTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
expect($result)->toEqual(1);
});

test('insert or ignore inserts data', function () {
test('insertOrIgnore inserts data', function () {
$characterData = [
"_key" => "LyannaStark",
"name" => "Lyanna",
Expand All @@ -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",
Expand All @@ -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" => "[email protected]",
];
DB::table('users')->insertOrIgnore($userData);

$result = DB::table('users')
->first();

expect($result->_id)->toBe('users/LyannaStark');

$userData = [
"username" => "Lya Stark",
"email" => "[email protected]",
];
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",
Expand All @@ -100,14 +125,14 @@
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');
});

0 comments on commit c302c4c

Please sign in to comment.