From b359b0dea5f44f65cabcc71a93bb5460680c279c Mon Sep 17 00:00:00 2001 From: Lukas Date: Wed, 17 May 2023 11:42:07 +0200 Subject: [PATCH 1/5] use `orderByRaw` for `integer` and `float` field types --- src/Entries/EntryQueryBuilder.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Entries/EntryQueryBuilder.php b/src/Entries/EntryQueryBuilder.php index 748ba895..a829919b 100644 --- a/src/Entries/EntryQueryBuilder.php +++ b/src/Entries/EntryQueryBuilder.php @@ -5,6 +5,7 @@ use Illuminate\Support\Str; use Statamic\Contracts\Entries\QueryBuilder; use Statamic\Entries\EntryCollection; +use Statamic\Facades\Collection; use Statamic\Facades\Entry; use Statamic\Query\EloquentQueryBuilder; use Statamic\Stache\Query\QueriesTaxonomizedEntries; @@ -18,6 +19,23 @@ class EntryQueryBuilder extends EloquentQueryBuilder implements QueryBuilder 'date', 'collection', 'created_at', 'updated_at', 'order', ]; + public function orderBy($column, $direction = 'asc') + { + $wheres = collect($this->builder->getQuery()->wheres); + + if ( + $collection = $wheres->firstWhere('column', 'collection') and + $field = Collection::find($collection['value'])->entryBlueprint()->fields()->get($column) and + in_array($field->config()['type'], ['integer', 'float']) + ) { + $this->builder->orderByRaw("data->'{$column}' {$direction}"); + + return $this; + } + + return parent::orderBy($column, $direction); + } + protected function transform($items, $columns = []) { $items = EntryCollection::make($items)->map(function ($model) use ($columns) { From 15f8e2636d4c8eae41de56ca375e82637dd961fe Mon Sep 17 00:00:00 2001 From: Lukas Date: Wed, 17 May 2023 12:20:44 +0200 Subject: [PATCH 2/5] don't access the `value` property if collection doesn't exist --- src/Entries/EntryQueryBuilder.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Entries/EntryQueryBuilder.php b/src/Entries/EntryQueryBuilder.php index a829919b..bf4686bf 100644 --- a/src/Entries/EntryQueryBuilder.php +++ b/src/Entries/EntryQueryBuilder.php @@ -24,8 +24,13 @@ public function orderBy($column, $direction = 'asc') $wheres = collect($this->builder->getQuery()->wheres); if ( - $collection = $wheres->firstWhere('column', 'collection') and - $field = Collection::find($collection['value'])->entryBlueprint()->fields()->get($column) and + ($collection = + $wheres->firstWhere('column', 'collection')['value'] ?? + null) and + ($field = Collection::find($collection) + ->entryBlueprint() + ->fields() + ->get($column)) and in_array($field->config()['type'], ['integer', 'float']) ) { $this->builder->orderByRaw("data->'{$column}' {$direction}"); From 4d2d1f88ea875324c668e24686604ec7fb167278 Mon Sep 17 00:00:00 2001 From: Ryan Mitchell Date: Mon, 19 Jun 2023 10:30:36 +0100 Subject: [PATCH 3/5] Change approach to cast dates, floats and integers --- src/Entries/EntryQueryBuilder.php | 56 ++++++++++++++------ tests/Data/Entries/EntryQueryBuilderTest.php | 51 ++++++++++++++++++ 2 files changed, 91 insertions(+), 16 deletions(-) diff --git a/src/Entries/EntryQueryBuilder.php b/src/Entries/EntryQueryBuilder.php index 9d4102bb..92daf51f 100644 --- a/src/Entries/EntryQueryBuilder.php +++ b/src/Entries/EntryQueryBuilder.php @@ -21,24 +21,48 @@ class EntryQueryBuilder extends EloquentQueryBuilder implements QueryBuilder public function orderBy($column, $direction = 'asc') { - $wheres = collect($this->builder->getQuery()->wheres); - - if ( - ($collection = - $wheres->firstWhere('column', 'collection')['value'] ?? - null) and - ($field = Collection::find($collection) - ->entryBlueprint() - ->fields() - ->get($column)) and - in_array($field->config()['type'], ['integer', 'float']) - ) { - $this->builder->orderByRaw("data->'{$column}' {$direction}"); - - return $this; + $actualColumn = $this->column($column); + + if (Str::contains($actualColumn, 'data->')) { + $wheres = collect($this->builder->getQuery()->wheres); + + if ($wheres->where('column', 'collection')->count() == 1) { + if ($collection = Collection::find($wheres->firstWhere('column', 'collection')['value'])) { + + // could limit by types here (float, integer, date) + $blueprintField = $collection->entryBlueprint()->fields()->get($column); // this assumes 1 blue print per collection... dont like it, maybe get all blueprints and merge any fields + if ($blueprintField) { + $castType = ''; + $fieldType = $blueprintField->type(); + + $grammar = $this->builder->getConnection()->getQueryGrammar(); + $actualColumn = $grammar->wrap($actualColumn); + + if (in_array($fieldType, ['float'])) { + $castType = 'float'; + } else if (in_array($fieldType, ['integer'])) { + $castType = 'float'; // bit sneaky but mysql doesnt support casting as integer, it wants unsigned + } else if (in_array($fieldType, ['date'])) { + $castType = 'date'; + + // sqlite casts dates to year, which is pretty unhelpful + if (str_contains(get_class($grammar), 'SQLiteGrammar')) { + $this->builder->orderByRaw("datetime({$actualColumn}) {$direction}"); + return $this; + } + } + + if ($castType) { + $this->builder->orderByRaw("cast({$actualColumn} as {$castType}) {$direction}"); + return $this; + } + } + } + } } - return parent::orderBy($column, $direction); + parent::orderBy($column, $direction); + return $this; } protected function transform($items, $columns = []) diff --git a/tests/Data/Entries/EntryQueryBuilderTest.php b/tests/Data/Entries/EntryQueryBuilderTest.php index c8ab112a..4ba9179a 100644 --- a/tests/Data/Entries/EntryQueryBuilderTest.php +++ b/tests/Data/Entries/EntryQueryBuilderTest.php @@ -726,4 +726,55 @@ public function entries_can_be_retrieved_on_join_table_conditions() // successfully retrieved 2 results $this->assertCount(2, $entries); } + + /** @test */ + public function entries_can_be_ordered_by_an_integer_json_field() + { + $blueprint = Blueprint::makeFromFields(['integer' => ['type' => 'integer',]]); + Blueprint::shouldReceive('in')->with('collections/posts')->andReturn(collect(['posts' => $blueprint])); + + Collection::make('posts')->save(); + EntryFactory::id('1')->slug('post-1')->collection('posts')->data(['title' => 'Post 1', 'integer' => 3])->create(); + EntryFactory::id('2')->slug('post-2')->collection('posts')->data(['title' => 'Post 2', 'integer' => 5])->create(); + EntryFactory::id('3')->slug('post-3')->collection('posts')->data(['title' => 'Post 3', 'integer' => 1])->create(); + + $entries = Entry::query()->where('collection', 'posts')->orderBy('integer', 'asc')->get(); + + $this->assertCount(3, $entries); + $this->assertEquals(['Post 3', 'Post 1', 'Post 2'], $entries->map->title->all()); + } + + /** @test */ + public function entries_can_be_ordered_by_an_float_json_field() + { + $blueprint = Blueprint::makeFromFields(['float' => ['type' => 'float',]]); + Blueprint::shouldReceive('in')->with('collections/posts')->andReturn(collect(['posts' => $blueprint])); + + Collection::make('posts')->save(); + EntryFactory::id('1')->slug('post-1')->collection('posts')->data(['title' => 'Post 1', 'float' => 3.3])->create(); + EntryFactory::id('2')->slug('post-2')->collection('posts')->data(['title' => 'Post 2', 'float' => 5.5])->create(); + EntryFactory::id('3')->slug('post-3')->collection('posts')->data(['title' => 'Post 3', 'float' => 1.1])->create(); + + $entries = Entry::query()->where('collection', 'posts')->orderBy('float', 'asc')->get(); + + $this->assertCount(3, $entries); + $this->assertEquals(['Post 3', 'Post 1', 'Post 2'], $entries->map->title->all()); + } + + /** @test */ + public function entries_can_be_ordered_by_an_date_json_field() + { + $blueprint = Blueprint::makeFromFields(['date_field' => ['type' => 'date', 'time_enabled' => true]]); + Blueprint::shouldReceive('in')->with('collections/posts')->andReturn(collect(['posts' => $blueprint])); + + Collection::make('posts')->save(); + EntryFactory::id('1')->slug('post-1')->collection('posts')->data(['title' => 'Post 1', 'date_field' => '2021-06-15 20:31:04'])->create(); + EntryFactory::id('2')->slug('post-2')->collection('posts')->data(['title' => 'Post 2', 'date_field' => '2021-01-13 20:31:04'])->create(); + EntryFactory::id('3')->slug('post-3')->collection('posts')->data(['title' => 'Post 3', 'date_field' => '2021-11-17 20:31:04'])->create(); + + $entries = Entry::query()->where('collection', 'posts')->orderBy('date_field', 'asc')->get(); + + $this->assertCount(3, $entries); + $this->assertEquals(['Post 2', 'Post 1', 'Post 3'], $entries->map->title->all()); + } } From 9ec96de6a9f0cb0be337f21e2cc288c020b9b0a0 Mon Sep 17 00:00:00 2001 From: Ryan Mitchell Date: Mon, 19 Jun 2023 10:31:35 +0100 Subject: [PATCH 4/5] StyleCI --- src/Entries/EntryQueryBuilder.php | 8 +++++--- tests/Data/Entries/EntryQueryBuilderTest.php | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Entries/EntryQueryBuilder.php b/src/Entries/EntryQueryBuilder.php index 92daf51f..f2b9b6b8 100644 --- a/src/Entries/EntryQueryBuilder.php +++ b/src/Entries/EntryQueryBuilder.php @@ -28,7 +28,6 @@ public function orderBy($column, $direction = 'asc') if ($wheres->where('column', 'collection')->count() == 1) { if ($collection = Collection::find($wheres->firstWhere('column', 'collection')['value'])) { - // could limit by types here (float, integer, date) $blueprintField = $collection->entryBlueprint()->fields()->get($column); // this assumes 1 blue print per collection... dont like it, maybe get all blueprints and merge any fields if ($blueprintField) { @@ -40,20 +39,22 @@ public function orderBy($column, $direction = 'asc') if (in_array($fieldType, ['float'])) { $castType = 'float'; - } else if (in_array($fieldType, ['integer'])) { + } elseif (in_array($fieldType, ['integer'])) { $castType = 'float'; // bit sneaky but mysql doesnt support casting as integer, it wants unsigned - } else if (in_array($fieldType, ['date'])) { + } elseif (in_array($fieldType, ['date'])) { $castType = 'date'; // sqlite casts dates to year, which is pretty unhelpful if (str_contains(get_class($grammar), 'SQLiteGrammar')) { $this->builder->orderByRaw("datetime({$actualColumn}) {$direction}"); + return $this; } } if ($castType) { $this->builder->orderByRaw("cast({$actualColumn} as {$castType}) {$direction}"); + return $this; } } @@ -62,6 +63,7 @@ public function orderBy($column, $direction = 'asc') } parent::orderBy($column, $direction); + return $this; } diff --git a/tests/Data/Entries/EntryQueryBuilderTest.php b/tests/Data/Entries/EntryQueryBuilderTest.php index 4ba9179a..02b0c518 100644 --- a/tests/Data/Entries/EntryQueryBuilderTest.php +++ b/tests/Data/Entries/EntryQueryBuilderTest.php @@ -730,7 +730,7 @@ public function entries_can_be_retrieved_on_join_table_conditions() /** @test */ public function entries_can_be_ordered_by_an_integer_json_field() { - $blueprint = Blueprint::makeFromFields(['integer' => ['type' => 'integer',]]); + $blueprint = Blueprint::makeFromFields(['integer' => ['type' => 'integer']]); Blueprint::shouldReceive('in')->with('collections/posts')->andReturn(collect(['posts' => $blueprint])); Collection::make('posts')->save(); @@ -747,7 +747,7 @@ public function entries_can_be_ordered_by_an_integer_json_field() /** @test */ public function entries_can_be_ordered_by_an_float_json_field() { - $blueprint = Blueprint::makeFromFields(['float' => ['type' => 'float',]]); + $blueprint = Blueprint::makeFromFields(['float' => ['type' => 'float']]); Blueprint::shouldReceive('in')->with('collections/posts')->andReturn(collect(['posts' => $blueprint])); Collection::make('posts')->save(); From 27b88ca05836bbcb35edeae435acedb1ee8f0b7c Mon Sep 17 00:00:00 2001 From: Ryan Mitchell Date: Mon, 19 Jun 2023 19:29:19 +0100 Subject: [PATCH 5/5] Don't assume we only have 1 blueprint per collection --- src/Entries/EntryQueryBuilder.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Entries/EntryQueryBuilder.php b/src/Entries/EntryQueryBuilder.php index f2b9b6b8..54d78a49 100644 --- a/src/Entries/EntryQueryBuilder.php +++ b/src/Entries/EntryQueryBuilder.php @@ -28,8 +28,17 @@ public function orderBy($column, $direction = 'asc') if ($wheres->where('column', 'collection')->count() == 1) { if ($collection = Collection::find($wheres->firstWhere('column', 'collection')['value'])) { - // could limit by types here (float, integer, date) - $blueprintField = $collection->entryBlueprint()->fields()->get($column); // this assumes 1 blue print per collection... dont like it, maybe get all blueprints and merge any fields + $blueprintField = $collection->entryBlueprints() + ->flatMap(function ($blueprint) { + return $blueprint->fields() + ->all() + ->filter(function ($field) { + return in_array($field->type(), ['float', 'integer', 'date']); + }); + }) + ->filter() + ->get($column); + if ($blueprintField) { $castType = ''; $fieldType = $blueprintField->type();