Skip to content

Commit

Permalink
Merge pull request #445: expose JSON methods
Browse files Browse the repository at this point in the history
  • Loading branch information
roxblnfk authored Nov 25, 2023
2 parents a6463cd + 13ba75e commit c8500a5
Show file tree
Hide file tree
Showing 11 changed files with 527 additions and 2 deletions.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"require": {
"php": ">=8.0",
"ext-pdo": "*",
"cycle/database": "^2.3",
"cycle/database": "^2.6",
"doctrine/instantiator": "^1.3.1 || ^2.0",
"spiral/core": "^2.8 || ^3.0"
},
Expand Down
12 changes: 12 additions & 0 deletions src/Select.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@
* @method $this orHaving(...$args);
* @method $this orderBy($expression, $direction = 'ASC');
* @method $this forUpdate()
* @method $this whereJson(string $path, mixed $value)
* @method $this orWhereJson(string $path, mixed $value)
* @method $this whereJsonContains(string $path, mixed $value, bool $encode = true, bool $validate = true)
* @method $this orWhereJsonContains(string $path, mixed $value, bool $encode = true, bool $validate = true)
* @method $this whereJsonDoesntContain(string $path, mixed $value, bool $encode = true, bool $validate = true)
* @method $this orWhereJsonDoesntContain(string $path, mixed $value, bool $encode = true, bool $validate = true)
* @method $this whereJsonContainsKey(string $path)
* @method $this orWhereJsonContainsKey(string $path)
* @method $this whereJsonDoesntContainKey(string $path)
* @method $this orWhereJsonDoesntContainKey(string $path)
* @method $this whereJsonLength(string $path, int $length, string $operator = '=')
* @method $this orWhereJsonLength(string $path, int $length, string $operator = '=')
* @method mixed avg($identifier) Perform aggregation (AVG) based on column or expression value.
* @method mixed min($identifier) Perform aggregation (MIN) based on column or expression value.
* @method mixed max($identifier) Perform aggregation (MAX) based on column or expression value.
Expand Down
13 changes: 13 additions & 0 deletions src/Select/QueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,19 @@
* @method QueryBuilder orderBy($expression, $direction = 'ASC');
* @method QueryBuilder limit(int $limit)
* @method QueryBuilder offset(int $offset)
* @method QueryBuilder forUpdate()
* @method QueryBuilder whereJson(string $path, mixed $value)
* @method QueryBuilder orWhereJson(string $path, mixed $value)
* @method QueryBuilder whereJsonContains(string $path, mixed $value, bool $encode = true, bool $validate = true)
* @method QueryBuilder orWhereJsonContains(string $path, mixed $value, bool $encode = true, bool $validate = true)
* @method QueryBuilder whereJsonDoesntContain(string $path, mixed $value, bool $encode = true, bool $validate = true)
* @method QueryBuilder orWhereJsonDoesntContain(string $path, mixed $value, bool $encode = true, bool $validate = true)
* @method QueryBuilder whereJsonContainsKey(string $path)
* @method QueryBuilder orWhereJsonContainsKey(string $path)
* @method QueryBuilder whereJsonDoesntContainKey(string $path)
* @method QueryBuilder orWhereJsonDoesntContainKey(string $path)
* @method QueryBuilder whereJsonLength(string $path, int $length, string $operator = '=')
* @method QueryBuilder orWhereJsonLength(string $path, int $length, string $operator = '=')
* @method int avg($identifier) Perform aggregation (AVG) based on column or expression value.
* @method int min($identifier) Perform aggregation (MIN) based on column or expression value.
* @method int max($identifier) Perform aggregation (MAX) based on column or expression value.
Expand Down
7 changes: 6 additions & 1 deletion src/Select/Traits/ColumnsTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ trait ColumnsTrait
*/
public function fieldAlias(string $field): ?string
{
return $this->columns[$field] ?? null;
// The field can be a JSON path separated by ->
$p = \explode('->', $field, 2);

$p[0] = $this->columns[$p[0]] ?? null;

return $p[0] === null ? null : \implode('->', $p);
}

/**
Expand Down
2 changes: 2 additions & 0 deletions tests/ORM/Fixtures/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ class User implements ImagedInterface
*/
public $credentials;

public ?string $settings = null;

public function __construct()
{
$this->posts = new ArrayCollection();
Expand Down
320 changes: 320 additions & 0 deletions tests/ORM/Functional/Driver/Common/Select/JsonMethodsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,320 @@
<?php

declare(strict_types=1);

namespace Cycle\ORM\Tests\Functional\Driver\Common\Select;

use Cycle\ORM\Mapper\Mapper;
use Cycle\ORM\Relation;
use Cycle\ORM\Schema;
use Cycle\ORM\Select;
use Cycle\ORM\Tests\Fixtures\Post;
use Cycle\ORM\Tests\Functional\Driver\Common\BaseTest;
use Cycle\ORM\Tests\Fixtures\User;
use Cycle\ORM\Tests\Traits\TableTrait;

abstract class JsonMethodsTest extends BaseTest
{
use TableTrait;

public function setUp(): void
{
parent::setUp();

$this->makeTable('users', ['id' => 'primary', 'user_settings' => 'json,nullable']);
$this->makeTable('posts', ['id' => 'primary', 'title' => 'string', 'user_id' => 'integer']);

$this->getDatabase()->table('users')->insertMultiple(
['user_settings'],
[[\json_encode(['theme' => 'dark', 'foo' => ['bar', 'baz']])], [\json_encode(['theme' => 'light'])]]
);
$this->getDatabase()->table('posts')->insertMultiple(['title', 'user_id'], [['Post 1', 1], ['Post 2', 2]]);

$this->orm = $this->withSchema(new Schema([
User::class => [
Schema::ROLE => 'user',
Schema::MAPPER => Mapper::class,
Schema::DATABASE => 'default',
Schema::TABLE => 'users',
Schema::PRIMARY_KEY => 'id',
Schema::COLUMNS => ['id' => 'id', 'settings' => 'user_settings'],
Schema::SCHEMA => [],
Schema::TYPECAST => ['id' => 'int'],
Schema::RELATIONS => [],
],
Post::class => [
Schema::ROLE => 'post',
Schema::MAPPER => Mapper::class,
Schema::DATABASE => 'default',
Schema::TABLE => 'posts',
Schema::PRIMARY_KEY => 'id',
Schema::COLUMNS => ['id', 'title', 'user_id'],
Schema::SCHEMA => [],
Schema::TYPECAST => ['id' => 'int'],
Schema::RELATIONS => [
'user' => [
Relation::TYPE => Relation::BELONGS_TO,
Relation::TARGET => 'user',
Relation::SCHEMA => [
Relation::CASCADE => true,
Relation::INNER_KEY => 'user_id',
Relation::OUTER_KEY => 'id',
],
],
],
],
]));
}

public function testWhereJson(): void
{
$selector = new Select($this->orm, User::class);
$user = $selector->whereJson('settings->theme', 'light')->fetchOne();

$this->assertSame(2, $user->id);
$this->assertEquals(['theme' => 'light'], \json_decode($user->settings, true));
}

public function testWhereJsonWithRelation(): void
{
$selector = new Select($this->orm, Post::class);
$post = $selector->whereJson('user.settings->theme', 'light')->fetchOne();

$this->assertSame(2, $post->id);
$this->assertEquals(['theme' => 'light'], \json_decode($post->user->settings, true));
}

public function testOrWhereJson(): void
{
$selector = new Select($this->orm, User::class);
$user = $selector
->where('id', 100)
->orWhereJson('settings->theme', 'light')
->fetchOne();

$this->assertSame(2, $user->id);
$this->assertEquals(['theme' => 'light'], \json_decode($user->settings, true));
}

public function testOrWhereJsonWithRelation(): void
{
$selector = new Select($this->orm, Post::class);
$post = $selector
->where('id', 100)
->orWhereJson('user.settings->theme', 'light')
->fetchOne();

$this->assertSame(2, $post->id);
$this->assertEquals(['theme' => 'light'], \json_decode($post->user->settings, true));
}

public function testWhereJsonContains(): void
{
$selector = new Select($this->orm, User::class);
$users = $selector->whereJsonContains('settings->foo', ['bar', 'baz'])->fetchAll();

$this->assertSame(1, $users[0]->id);
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], \json_decode($users[0]->settings, true));
}

public function testWhereJsonContainsWithRelation(): void
{
$selector = new Select($this->orm, Post::class);
$posts = $selector->whereJsonContains('user.settings->foo', ['bar', 'baz'])->fetchAll();

$this->assertSame(1, $posts[0]->id);
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], \json_decode($posts[0]->user->settings, true));
}

public function testOrWhereJsonContains(): void
{
$selector = new Select($this->orm, User::class);
$users = $selector
->where('id', 100)
->orWhereJsonContains('user.settings->foo', ['bar', 'baz'])
->fetchAll();

$this->assertSame(1, $users[0]->id);
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], \json_decode($users[0]->settings, true));
}

public function testOrWhereJsonContainsWithRelation(): void
{
$selector = new Select($this->orm, Post::class);
$posts = $selector
->where('id', 100)
->orWhereJsonContains('user.settings->foo', ['bar', 'baz'])
->fetchAll();

$this->assertSame(1, $posts[0]->id);
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], \json_decode($posts[0]->user->settings, true));
}

public function testWhereJsonDoesntContain(): void
{
$selector = new Select($this->orm, User::class);
$users = $selector->whereJsonDoesntContain('settings->theme', 'light')->fetchAll();

$this->assertSame(1, $users[0]->id);
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], \json_decode($users[0]->settings, true));
}

public function testWhereJsonDoesntContainWithRelation(): void
{
$selector = new Select($this->orm, Post::class);
$posts = $selector->whereJsonDoesntContain('user.settings->theme', 'light')->fetchAll();

$this->assertSame(1, $posts[0]->id);
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], \json_decode($posts[0]->user->settings, true));
}

public function testOrWhereJsonDoesntContain(): void
{
$selector = new Select($this->orm, User::class);
$users = $selector
->where('id', 100)
->orWhereJsonDoesntContain('settings->theme', 'light')
->fetchAll();

$this->assertSame(1, $users[0]->id);
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], \json_decode($users[0]->settings, true));
}

public function testOrWhereJsonDoesntContainWithRelation(): void
{
$selector = new Select($this->orm, Post::class);
$posts = $selector
->where('id', 100)
->orWhereJsonDoesntContain('user.settings->theme', 'light')
->fetchAll();

$this->assertSame(1, $posts[0]->id);
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], \json_decode($posts[0]->user->settings, true));
}

public function testWhereJsonContainsKey(): void
{
$selector = new Select($this->orm, User::class);
$user = $selector->whereJsonContainsKey('settings->foo')->fetchOne();

$this->assertSame(1, $user->id);
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], \json_decode($user->settings, true));
}

public function testWhereJsonContainsKeyWithRelation(): void
{
$selector = new Select($this->orm, Post::class);
$post = $selector->whereJsonContainsKey('user.settings->foo')->fetchOne();

$this->assertSame(1, $post->id);
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], \json_decode($post->user->settings, true));
}

public function testOrWhereJsonContainsKey(): void
{
$selector = new Select($this->orm, User::class);
$user = $selector
->where('id', 100)
->orWhereJsonContainsKey('settings->foo')
->fetchOne();

$this->assertSame(1, $user->id);
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], \json_decode($user->settings, true));
}

public function testOrWhereJsonContainsKeyWithRelation(): void
{
$selector = new Select($this->orm, Post::class);
$post = $selector
->where('id', 100)
->orWhereJsonContainsKey('user.settings->foo')
->fetchOne();

$this->assertSame(1, $post->id);
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], \json_decode($post->user->settings, true));
}

public function testWhereJsonDoesntContainKey(): void
{
$selector = new Select($this->orm, User::class);
$user = $selector->whereJsonDoesntContainKey('settings->foo')->fetchOne();

$this->assertSame(2, $user->id);
$this->assertEquals(['theme' => 'light'], \json_decode($user->settings, true));
}

public function testWhereJsonDoesntContainKeyWithRelation(): void
{
$selector = new Select($this->orm, Post::class);
$post = $selector->whereJsonDoesntContainKey('user.settings->foo')->fetchOne();

$this->assertSame(2, $post->id);
$this->assertEquals(['theme' => 'light'], \json_decode($post->user->settings, true));
}

public function testOrWhereJsonDoesntContainKey(): void
{
$selector = new Select($this->orm, User::class);
$user = $selector
->where('id', 100)
->orWhereJsonDoesntContainKey('settings->foo')
->fetchOne();

$this->assertSame(2, $user->id);
$this->assertEquals(['theme' => 'light'], \json_decode($user->settings, true));
}

public function testOrWhereJsonDoesntContainKeyWithRelation(): void
{
$selector = new Select($this->orm, Post::class);
$post = $selector
->where('id', 100)
->orWhereJsonDoesntContainKey('user.settings->foo')
->fetchOne();

$this->assertSame(2, $post->id);
$this->assertEquals(['theme' => 'light'], \json_decode($post->user->settings, true));
}

public function testWhereJsonLength(): void
{
$selector = new Select($this->orm, User::class);
$user = $selector->whereJsonLength('settings->foo', 2)->fetchOne();

$this->assertSame(1, $user->id);
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], \json_decode($user->settings, true));
}

public function testWhereJsonLengthWithRelation(): void
{
$selector = new Select($this->orm, Post::class);
$post = $selector->whereJsonLength('user.settings->foo', 2)->fetchOne();

$this->assertSame(1, $post->id);
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], \json_decode($post->user->settings, true));
}

public function testOrWhereJsonLength(): void
{
$selector = new Select($this->orm, User::class);
$user = $selector
->where('id', 100)
->orWhereJsonLength('settings->foo', 2)
->fetchOne();

$this->assertSame(1, $user->id);
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], \json_decode($user->settings, true));
}

public function testOrWhereJsonLengthWithRelation(): void
{
$selector = new Select($this->orm, Post::class);
$post = $selector
->where('id', 100)
->orWhereJsonLength('user.settings->foo', 2)
->fetchOne();

$this->assertSame(1, $post->id);
$this->assertEquals(['theme' => 'dark', 'foo' => ['bar', 'baz']], \json_decode($post->user->settings, true));
}
}
Loading

0 comments on commit c8500a5

Please sign in to comment.