Skip to content

Commit

Permalink
Add more tests and columns transformer
Browse files Browse the repository at this point in the history
  • Loading branch information
ahawlitschek committed Mar 22, 2024
1 parent cd9e914 commit a7661b0
Show file tree
Hide file tree
Showing 8 changed files with 674 additions and 104 deletions.
69 changes: 69 additions & 0 deletions src/Eloquent/ColumnsTransformator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

namespace Clickbar\LaravelPowerRelations\Eloquent;

use Illuminate\Contracts\Database\Query\Expression;
use Illuminate\Database\Grammar;
use Illuminate\Support\Str;

/**
* The Class is used to transform the $columns given to the getRelationExistenceQuery method of a power relation.
*/
class ColumnsTransformator
{
const defaultAggregates = [
'sum',
'avg',
'min',
'max',
'count',
];

private static function isAggregate(string $sql): bool
{
foreach (self::defaultAggregates as $aggregate) {
if (str_starts_with(strtolower($sql), strtolower($aggregate))) {
return true;
}
}

return false;
}

public static function transform(mixed $columns, string $tableName, Grammar $grammar): mixed
{
if (is_array($columns)) {
return array_map(fn ($column) => self::transform($column, $tableName, $grammar), $columns);
}

if ($columns instanceof Expression) {
return self::transform($columns->getValue($grammar), $tableName, $grammar);
}

if (is_string($columns) && self::isAggregate($columns)) {
return new \Illuminate\Database\Query\Expression(self::transformAggregate($columns, $tableName));
}

return $columns;
}

private static function transformAggregate(string $sql, string $tableName): string
{

// Retrieve the column after the last "."
$column = Str::of($sql)
->between('(', ')')
->afterLast('"."')
->trim('"')
->toString();

// In case of only the * we do not need to prefix it with a table name
if ($column === '*') {
return $sql;
}

$aggregate = Str::before($sql, '(');

return "$aggregate(\"$tableName\".\"$column\")";
}
}
8 changes: 8 additions & 0 deletions src/Eloquent/Relation/PowerRelation.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Clickbar\LaravelPowerRelations\Eloquent\Relation;

use Clickbar\LaravelPowerRelations\Eloquent\ColumnsTransformator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
Expand Down Expand Up @@ -113,6 +114,13 @@ public function getResults()
*/
public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']): Builder
{
/*
* There might come some aggregates with column specified, so we need to transform the table name.
* Before transformation the table name is guessed by the $table of the target eloquent model.
* Since we use a fromSub call, we need to change the table in the aggregate function.
*/
$columns = ColumnsTransformator::transform($columns, 'dynamic_relation_correlated', $query->getGrammar());

$subSelect = $this->relatedInstance->newQuery();
if ($this->queryFromParent) {
$subSelect->from($this->parent->getTable());
Expand Down
111 changes: 111 additions & 0 deletions tests/RelationDeleteTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php

use Clickbar\LaravelPowerRelations\Tests\TestClasses\Models\Client;
use Clickbar\LaravelPowerRelations\Tests\TestClasses\Models\Order;
use Clickbar\LaravelPowerRelations\Tests\TestClasses\Models\Project;
use Clickbar\LaravelPowerRelations\Tests\TestClasses\Models\Task;
use Clickbar\LaravelPowerRelations\Tests\TestClasses\Models\TaskWithSoftDelete;

it('can delete tasks of client via PowerRelation', function () {

// Create some noise
Client::factory()->count(3)
->has(Project::factory()->count(7)
->has(Order::factory()->count(4)
->has(Task::factory()->count(8))
)
)
->create();

// Create the data we will expect
$client = Client::factory()->create();
$projects = Project::factory()->count(2)->recycle($client)->create();
$orders = $projects->flatMap(fn (Project $project) => Order::factory()->count(2)->recycle($project)->create());
$tasks = $orders->flatMap(fn (Order $order) => Task::factory()->count(3)->recycle($order)->create());

expect(Task::whereIn('order_id', $orders->pluck('id'))->count())->toBeGreaterThan(0);
$otherTaskCount = Task::whereNotIn('order_id', $orders->pluck('id'))->count();

$client->tasks()->delete();
expect(Task::all())->toHaveCount($otherTaskCount);
expect(Task::whereIn('order_id', $orders->pluck('id'))->get())->toBeEmpty();
});

it('can delete tasks of client via PowerRelation from Parent', function () {

// Create some noise
Client::factory()->count(3)
->has(Project::factory()->count(7)
->has(Order::factory()->count(4)
->has(Task::factory()->count(8))
)
)
->create();

// Create the data we will expect
$client = Client::factory()->create();
$projects = Project::factory()->count(2)->recycle($client)->create();
$orders = $projects->flatMap(fn (Project $project) => Order::factory()->count(2)->recycle($project)->create());
$tasks = $orders->flatMap(fn (Order $order) => Task::factory()->count(3)->recycle($order)->create());

expect(Task::whereIn('order_id', $orders->pluck('id'))->count())->toBeGreaterThan(0);
$otherTaskCount = Task::whereNotIn('order_id', $orders->pluck('id'))->count();

$client->tasksFromParent()->delete();
expect(Task::all())->toHaveCount($otherTaskCount);
expect(Task::whereIn('order_id', $orders->pluck('id'))->get())->toBeEmpty();
});

it('can soft delete tasks of client via PowerRelation', function () {

// Create some noise
Client::factory()->count(3)
->has(Project::factory()->count(7)
->has(Order::factory()->count(4)
->has(TaskWithSoftDelete::factory()->count(8), 'tasksWithSoftDelete')
)
)
->create();

// Create the data we will expect
$client = Client::factory()->create();
$projects = Project::factory()->count(2)->recycle($client)->create();
$orders = $projects->flatMap(fn (Project $project) => Order::factory()->count(2)->recycle($project)->create());
$tasks = $orders->flatMap(fn (Order $order) => TaskWithSoftDelete::factory()->count(3)->recycle($order)->create());

expect(TaskWithSoftDelete::whereIn('order_id', $orders->pluck('id'))->count())->toBeGreaterThan(0);
$otherTaskCount = TaskWithSoftDelete::whereNotIn('order_id', $orders->pluck('id'))->count();

$client->tasksWithSoftDelete()->delete();

expect(TaskWithSoftDelete::all())->toHaveCount($otherTaskCount);
expect(TaskWithSoftDelete::whereIn('order_id', $orders->pluck('id'))->get())->toBeEmpty();
expect(TaskWithSoftDelete::whereIn('order_id', $orders->pluck('id'))->withTrashed()->get())->toHaveCount($tasks->count());
});

it('can soft delete tasks of client via PowerRelation from parent', function () {

// Create some noise
Client::factory()->count(3)
->has(Project::factory()->count(7)
->has(Order::factory()->count(4)
->has(TaskWithSoftDelete::factory()->count(8), 'tasksWithSoftDelete')
)
)
->create();

// Create the data we will expect
$client = Client::factory()->create();
$projects = Project::factory()->count(2)->recycle($client)->create();
$orders = $projects->flatMap(fn (Project $project) => Order::factory()->count(2)->recycle($project)->create());
$tasks = $orders->flatMap(fn (Order $order) => TaskWithSoftDelete::factory()->count(3)->recycle($order)->create());

expect(TaskWithSoftDelete::whereIn('order_id', $orders->pluck('id'))->count())->toBeGreaterThan(0);
$otherTaskCount = TaskWithSoftDelete::whereNotIn('order_id', $orders->pluck('id'))->count();

$client->tasksWithSoftDeleteFromParent()->delete();

expect(TaskWithSoftDelete::all())->toHaveCount($otherTaskCount);
expect(TaskWithSoftDelete::whereIn('order_id', $orders->pluck('id'))->get())->toBeEmpty();
expect(TaskWithSoftDelete::whereIn('order_id', $orders->pluck('id'))->withTrashed()->get())->toHaveCount($tasks->count());
});
94 changes: 94 additions & 0 deletions tests/RelationEagerLoadTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php

use Clickbar\LaravelPowerRelations\Tests\TestClasses\Models\Client;
use Clickbar\LaravelPowerRelations\Tests\TestClasses\Models\Order;
use Clickbar\LaravelPowerRelations\Tests\TestClasses\Models\Project;
use Clickbar\LaravelPowerRelations\Tests\TestClasses\Models\Task;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

it('can eager load tasks of client via PowerRelation', function () {

// Create some noise
Client::factory()->count(3)
->has(Project::factory()->count(7)
->has(Order::factory()->count(4)
->has(Task::factory()->count(8))
)
)
->create();

DB::enableQueryLog();
$clients = Client::with('tasks')->get();
expect(DB::getQueryLog())->toHaveCount(2);

// Load it for the flat way for comparison
$clients->load('projects.orders.tasks');

DB::flushQueryLog();
foreach ($clients as $client) {
/** @var Collection<int, Task> $tasksFromFlatWay */
$tasksFromFlatWay = $client->tasks_from_relation_chain;
/** @var Collection<int, Task> $tasksFromEagerLoad */
$tasksFromEagerLoad = $client->tasks;

expect($tasksFromFlatWay)->toHaveSameSize($tasksFromEagerLoad);

// Compare the task with its key => values
$tasksFromEagerLoad->zip($tasksFromFlatWay)->each(function ($data) {
$eagerTaskData = $data->get(0)->toArray();
$flatTaskData = $data->get(1)->toArray();

ksort($flatTaskData);
ksort($eagerTaskData);

// Use toMatchArray, because the eager loads adds the clients.id property to the attributes
expect($eagerTaskData)->toMatchArray($flatTaskData);
});
}
expect(DB::getQueryLog())->toBeEmpty();

});

it('can eager load tasks of client via PowerRelation from Parent', function () {

// Create some noise
Client::factory()->count(3)
->has(Project::factory()->count(7)
->has(Order::factory()->count(4)
->has(Task::factory()->count(8))
)
)
->create();

DB::enableQueryLog();
$clients = Client::with('tasksFromParent')->get();
expect(DB::getQueryLog())->toHaveCount(2);

// Load it for the flat way for comparison
$clients->load('projects.orders.tasks');

DB::flushQueryLog();
foreach ($clients as $client) {
/** @var Collection<int, Task> $tasksFromFlatWay */
$tasksFromFlatWay = $client->tasks_from_relation_chain;
/** @var Collection<int, Task> $tasksFromEagerLoad */
$tasksFromEagerLoad = $client->tasksFromParent;

expect($tasksFromFlatWay)->toHaveSameSize($tasksFromEagerLoad);

// Compare the task with its key => values
$tasksFromEagerLoad->zip($tasksFromFlatWay)->each(function ($data) {
$eagerTaskData = $data->get(0)->toArray();
$flatTaskData = $data->get(1)->toArray();

ksort($flatTaskData);
ksort($eagerTaskData);

// Use toMatchArray, because the eager loads adds the clients.id property to the attributes
expect($eagerTaskData)->toMatchArray($flatTaskData);
});
}
expect(DB::getQueryLog())->toBeEmpty();

});
Loading

0 comments on commit a7661b0

Please sign in to comment.