From 139504f8dde7d6807fa31a474374908beb0df374 Mon Sep 17 00:00:00 2001
From: Dave Mc Nicoll <dave.mcnicoll@cslsj.qc.ca>
Date: Tue, 6 Oct 2020 11:00:12 -0400
Subject: [PATCH] - Added the HAVING and TRUNCATE keyword - Fixed GroupBy which
 was not implemented yet - Added a new SQL raw() function - From statement now
 accepts subqueries

---
 src/Common/Sql.php                            |   5 +
 src/EntityCollection.php                      |  50 ++++
 src/EntityTrait.php                           |   9 +-
 .../CollectionFromQueryInterface.php}         |   4 +-
 src/Query/Fragment.php                        |  16 ++
 src/Query/From.php                            |  15 +-
 src/Query/GroupBy.php                         |  13 +-
 src/Query/Having.php                          | 141 ++++++++-
 src/Query/Join.php                            |  24 +-
 src/Query/OrderBy.php                         |   2 +-
 src/Query/Select.php                          |   9 +
 src/Query/Truncate.php                        |  26 ++
 src/Query/Where.php                           |   6 +-
 src/QueryBuilder.php                          |  75 +++--
 src/Repository.php                            | 267 +++++-------------
 src/Repository/ConditionTrait.php             | 153 ++++++++++
 .../SearchRequestPaginationTrait.php          |   2 +
 17 files changed, 566 insertions(+), 251 deletions(-)
 rename src/Event/{RepositoryCollectionFromQueryInterface.php => Repository/CollectionFromQueryInterface.php} (59%)
 create mode 100644 src/Query/Truncate.php
 create mode 100644 src/Repository/ConditionTrait.php

diff --git a/src/Common/Sql.php b/src/Common/Sql.php
index a03ed06..1d5c01c 100644
--- a/src/Common/Sql.php
+++ b/src/Common/Sql.php
@@ -55,6 +55,11 @@ abstract class Sql {
             } 
         };
     }
+    
+    public static function raw(string $sql) : object
+    {
+        return static::identifier($sql);
+    }
 
     public static function escape($value)
     {
diff --git a/src/EntityCollection.php b/src/EntityCollection.php
index 91ea2e5..42faa81 100644
--- a/src/EntityCollection.php
+++ b/src/EntityCollection.php
@@ -6,6 +6,8 @@ use Generator;
 
 class EntityCollection extends \ArrayObject {
     
+    public ? string $entityClass = null;
+    
     public function filters(Callable $callback) : Generator
     {
         $idx = 0;
@@ -29,6 +31,17 @@ class EntityCollection extends \ArrayObject {
         
         return $collection;
     }
+        
+    public function iterate(Callable $callback) : array
+    {
+        $results = [];
+        
+        foreach($this as $item) {
+            $results[] = $callback($item);
+        }
+        
+        return $results;
+    }
     
     public function removeOne($value, string $field, bool $strict = true) : ? object
     {
@@ -72,6 +85,17 @@ class EntityCollection extends \ArrayObject {
         }
     }
     
+    public function column($field) : array
+    {
+        $list = [];
+        
+        foreach($this as $item) {
+            $list[] = $item->$field;
+        }
+        
+        return $list;
+    }
+    
     public function buildArray(string $keyColumn, /* string|callable|null */ $value = null) : array
     {
         $list = [];
@@ -107,4 +131,30 @@ class EntityCollection extends \ArrayObject {
         
         return $list;
     }
+    
+    public function fromArray(array $datasets, ? string $entityClass = null) : self 
+    {
+        if ( ! ($this->entityClass || $entityClass) ) {
+            throw new \Exception("An entity class name must be provided to be instanciated and populated before insertion into this collection.");
+        }
+        
+        $className = $entityClass ?: $this->entityClass;
+        
+        foreach($datasets as $dataset) {
+            $this->append( (new $className() )->fromArray($dataset) );
+        }
+        
+        return $this;
+    }
+    
+    public function mergeWith( /*array|EntityCollection*/ $datasets ) : self
+    {
+        if ( is_object($datasets) ) {
+            $datasets = $datasets->getArrayCopy();
+        }
+        
+        $this->exchangeArray( array_merge( $this->getArrayCopy(), $datasets ) );
+        
+        return $this;
+    }
 }
diff --git a/src/EntityTrait.php b/src/EntityTrait.php
index c19077e..a85e5e5 100644
--- a/src/EntityTrait.php
+++ b/src/EntityTrait.php
@@ -321,9 +321,9 @@ trait EntityTrait {
     /**
      * @Ignore
      */
-    public function toCollection() : array
+    public function toCollection() : EntityCollection
     {
-        return new EntityCollection($this->toArray());
+        return static::entityCollection([ $this->toArray() ]);
     }
     
     /**
@@ -377,7 +377,10 @@ trait EntityTrait {
      */
     public static function entityCollection(...$arguments) : EntityCollection
     {
-        return new EntityCollection(...$arguments);
+        $collection = new EntityCollection(...$arguments);
+        $collection->entityClass = static::class;
+        
+        return $collection;
     }
 
     /**
diff --git a/src/Event/RepositoryCollectionFromQueryInterface.php b/src/Event/Repository/CollectionFromQueryInterface.php
similarity index 59%
rename from src/Event/RepositoryCollectionFromQueryInterface.php
rename to src/Event/Repository/CollectionFromQueryInterface.php
index 1c167e4..b18fa89 100644
--- a/src/Event/RepositoryCollectionFromQueryInterface.php
+++ b/src/Event/Repository/CollectionFromQueryInterface.php
@@ -1,9 +1,9 @@
 <?php
 
-namespace Ulmus\Event;
+namespace Ulmus\Event\Repository;
 
 use Ulmus\EntityCollection;
 
-interface RepositoryCollectionFromQueryInterface {
+interface CollectionFromQueryInterface {
     public function execute(EntityCollection $collection) : EntityCollection;    
 }
diff --git a/src/Query/Fragment.php b/src/Query/Fragment.php
index 7754a2c..5dd7d23 100644
--- a/src/Query/Fragment.php
+++ b/src/Query/Fragment.php
@@ -12,4 +12,20 @@ abstract class Fragment {
     {
         return implode($glue, array_filter($segments, function($i) { return ! is_null($i) && $i !== false && $i !== ""; }));
     }
+    
+    protected function renderTables(/*array|string*/ $tableList, ? int $maxTableCount = null) : string
+    {
+        $i = 0;
+        $list = [];
+
+        foreach((array) $tableList as $alias => $table) {
+            $list[] = ! is_numeric($alias) ? "$table $alias" : $table;
+            
+            if ( ++$i === $maxTableCount ) {
+                break;
+            }
+        }
+
+        return implode(", ", $list);
+    }
 }
diff --git a/src/Query/From.php b/src/Query/From.php
index 3d291a0..3ff11ee 100644
--- a/src/Query/From.php
+++ b/src/Query/From.php
@@ -10,6 +10,8 @@ class From extends Fragment {
 
     public array $tables = [];
 
+    public string $sql;
+    
     public function set(array $tables) : self
     {
         $this->tables = $tables;
@@ -29,18 +31,7 @@ class From extends Fragment {
     public function render() : string
     {
         return $this->renderSegments([
-            static::SQL_TOKEN, $this->renderTables(),
+            static::SQL_TOKEN, $this->sql ?? $this->renderTables($this->tables),
         ]);
     }
-
-    protected function renderTables() : string
-    {
-        $list = [];
-
-        foreach((array) $this->tables as $alias => $table) {
-            $list[] = ! is_numeric($alias) ? "$table $alias" : $table;
-        }
-
-        return implode(", ", $list);
-    }
 }
diff --git a/src/Query/GroupBy.php b/src/Query/GroupBy.php
index 7a05b4b..fd969cf 100644
--- a/src/Query/GroupBy.php
+++ b/src/Query/GroupBy.php
@@ -3,22 +3,23 @@
 namespace Ulmus\Query;
 
 class GroupBy extends Fragment {
-    
-    const SQL_TOKEN = "GROUP BY";
-    
-    public int $order = 70;
+    public int $order = 75;
 
     public array $groupBy = [];
+    
+    const SQL_TOKEN = "GROUP BY";
 
     public function set(array $order) : self
     {
-        $this->groupBy = $order;
+        $this->groupBy = [ $order ];
+        
         return $this;
     }
 
-    public function add(string $field, ? string $direction = null) : self
+    public function add(string $field) : self
     {
         $this->groupBy[] = $field;
+        
         return $this;
     }
 
diff --git a/src/Query/Having.php b/src/Query/Having.php
index 5d349cd..166a56a 100644
--- a/src/Query/Having.php
+++ b/src/Query/Having.php
@@ -2,7 +2,142 @@
 
 namespace Ulmus\Query;
 
-class Having extends Where {
-    const SQL_TOKEN = "HAVING";
-    public int $order = 75;
+use Ulmus\QueryBuilder;
+
+use Ulmus\Common\EntityField,
+    Ulmus\Common\Sql;
+
+class Having extends Fragment {
+    const SQL_TOKEN = "HAVING"; 
+
+    public int $order = 80;
+
+    public array $conditionList;
+
+    public QueryBuilder $queryBuilder;
+
+    public ? Having $parent = null;
+
+    public string $condition = Where::CONDITION_AND;
+
+    public function __construct(? QueryBuilder $queryBuilder, $condition = Where::CONDITION_AND)
+    {
+        $this->queryBuilder = $queryBuilder;
+        $this->condition = $condition;
+        $this->parent = $queryBuilder->having ?? null;
+    }
+
+    public function add($field, $value, string $operator, string $condition, bool $not = false) : self
+    {
+        $this->conditionList[] = [
+            $field,
+            $value,
+            $operator ?: $this->queryBuilder->conditionOperator,
+            $condition,
+            $not
+        ];
+
+        return $this;
+    }
+
+    public function render(bool $skipToken = false) : string
+    {
+        $stack = [];
+        
+        foreach ($this->conditionList ?? [] as $key => $item) {
+            if ( $item instanceof static ) {
+                if ( $item->conditionList ?? false ) {
+                    $stack[] = ( $key !== 0 ? "{$item->condition} " : "" ) . "(" . $item->render($skipToken) . ")";
+                }
+            }
+            else {
+                list($field, $value, $operator, $condition, $not) = $item;
+                $stack[] = $latest = $this->havingCondition($field, $value, $operator, $key !== 0 ? $condition : "", $not);
+            }
+        }
+
+        return $this->renderSegments([
+            ! $this->parent && ! $skipToken ? static::SQL_TOKEN : "",
+            implode(" ", $stack)
+        ]);
+    }
+
+    protected function havingCondition($field, $value, string $operator = Where::OPERATOR_EQUAL, string $condition = Where::CONDITION_AND, bool $not = false) {
+        return new class($this->queryBuilder, $field, $value, $operator, $condition, $not) {
+
+            public $value;
+            public bool $not = false;
+            public string $field;
+            public string $operator;
+            public string $condition;
+            public QueryBuilder $queryBuilder;
+
+            protected string $content = "";
+
+            public function __construct(QueryBuilder $queryBuilder, string $field, $value, string $operator, string $condition, bool $not) {
+                $this->queryBuilder = $queryBuilder;
+                $this->field = $field;
+                $this->value = $value;
+                $this->condition = $condition;
+                $this->operator = $operator;
+                $this->not = $not;
+            }
+
+            public function render() : string
+            {
+                $value = $this->value();
+
+                return $this->content ?: $this->content = implode(" ", array_filter([
+                    $this->condition,
+                    $this->not ? Where::CONDITION_NOT : "",
+                    $this->field,
+                    $this->operator(),
+                    $value,
+                ]));
+            }
+
+            protected function operator() : string
+            {
+                if ( is_array($this->value) ) {
+                    return (in_array($this->operator, [ '!=', '<>' ]) ? Where::CONDITION_NOT . " " : "") . Where::COMPARISON_IN;
+                }
+
+                return $this->operator;
+            }
+
+            protected function value()
+            {
+                if ( is_array($this->value) ) {
+                    $stack = [];
+
+                    foreach($this->value as $item) {
+                        $stack[] = $this->filterValue($item);
+                    }
+
+                    return "(" . implode(", ", $stack) . ")";
+                }
+
+                return $this->filterValue($this->value);
+            }
+
+            protected function filterValue($value)
+            {
+                if ( $value === null ) {
+                    $this->operator = in_array($this->operator, [ '!=', '<>' ]) ? Where::COMPARISON_IS . " " . Where::CONDITION_NOT : Where::COMPARISON_IS;
+                    return Where::COMPARISON_NULL;
+                }
+                elseif ( is_object($value) && ( $value instanceof EntityField ) ) {
+                    return $value->name();
+                }
+                else {
+                    return $this->queryBuilder->addParameter($value);
+                }
+            }
+
+            public function __toString() : string
+            {
+                return $this->render();
+            }
+        };
+    }
 }
diff --git a/src/Query/Join.php b/src/Query/Join.php
index 238a3f4..58a0726 100644
--- a/src/Query/Join.php
+++ b/src/Query/Join.php
@@ -3,9 +3,13 @@
 namespace Ulmus\Query;
 
 use Ulmus\QueryBuilder;
+use Ulmus\Repository\ConditionTrait;
 
-class Join extends Fragment {
+class Join extends Fragment 
+{
+    use ConditionTrait;
     
+    const SQL_OUTER = "OUTER";
     const SQL_TOKEN = "JOIN";
 
     const TYPE_LEFT = "LEFT";
@@ -33,8 +37,10 @@ class Join extends Fragment {
     
     public /* QueryBuilder */ $queryBuilder;
 
-    public function __construct(QueryBuilder $queryBuilder) {
-        $this->queryBuilder = $queryBuilder;
+    public function __construct(QueryBuilder $queryBuilder) 
+    {
+        $this->queryBuilder = new QueryBuilder();
+        $this->queryBuilder->parent = $queryBuilder;
     }
     
     public function set(string $side, /* QueryBuilder|string */ $table, string $field, /* QueryBuilder|string */ $value) 
@@ -44,9 +50,15 @@ class Join extends Fragment {
         $this->field = $field;
         $this->value = $value;
     }
-    
+
     public function render() : string
     {
-        return $this->renderSegments([ strtoupper($this->side), static::SQL_TOKEN, $this->table, $this->alias ?? "", $this->attachment, $this->field, "=", $this->value ]);
+        if ($this->queryBuilder->where ?? false ) {
+            $where = $this->renderSegments([Where::CONDITION_AND, $this->queryBuilder->render(true)]);
+        }
+        
+        return $this->renderSegments([ 
+            strtoupper($this->side), $this->outer ? static::SQL_OUTER : "", static::SQL_TOKEN, $this->table, $this->alias ?? "", $this->attachment, $this->field, "=", $this->value, $where ?? ""
+        ]);
     }
-}
+}
\ No newline at end of file
diff --git a/src/Query/OrderBy.php b/src/Query/OrderBy.php
index 5e62ad2..318690a 100644
--- a/src/Query/OrderBy.php
+++ b/src/Query/OrderBy.php
@@ -3,7 +3,7 @@
 namespace Ulmus\Query;
 
 class OrderBy extends Fragment {
-    public int $order = 80;
+    public int $order = 85;
 
     public array $orderBy = [];
     
diff --git a/src/Query/Select.php b/src/Query/Select.php
index ceecf77..aa2f3d8 100644
--- a/src/Query/Select.php
+++ b/src/Query/Select.php
@@ -4,6 +4,8 @@ namespace Ulmus\Query;
 
 class Select extends Fragment {
 
+    const FIELD_AS = "%s as %s";
+    
     public int $order = -100;
 
     public bool $distinct = false;
@@ -36,6 +38,13 @@ class Select extends Fragment {
 
     public function render() : string
     {
+        foreach($this->fields as $key => &$value) {
+            if ( ! is_numeric($key) ) {
+                $value = sprintf(static::FIELD_AS, $value, $key);
+            }
+            
+        }
+        
         return $this->renderSegments([
             ( $this->union ? 'UNION' : false ),
             static::SQL_TOKEN,
diff --git a/src/Query/Truncate.php b/src/Query/Truncate.php
new file mode 100644
index 0000000..da26663
--- /dev/null
+++ b/src/Query/Truncate.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Ulmus\Query;
+
+class Truncate extends Fragment {
+
+    const SQL_TOKEN = "TRUNCATE TABLE";
+    
+    public int $order = -100;
+
+    public string $table;
+    
+    public function set(string $tableName) : self
+    {
+        $this->table = $tableName;
+        
+        return $this;
+    }
+
+    public function render() : string
+    {
+        return $this->renderSegments([
+            static::SQL_TOKEN, $this->table,
+        ]);
+    }
+}
diff --git a/src/Query/Where.php b/src/Query/Where.php
index 736d5ec..255dffe 100644
--- a/src/Query/Where.php
+++ b/src/Query/Where.php
@@ -50,14 +50,14 @@ class Where extends Fragment {
         return $this;
     }
 
-    public function render() : string
+    public function render(bool $skipToken = false) : string
     {
         $stack = [];
         
         foreach ($this->conditionList ?? [] as $key => $item) {
             if ( $item instanceof Where ) {
                 if ( $item->conditionList ?? false ) {
-                    $stack[] = ( $key !== 0 ? "{$item->condition} " : "" ) . "(" . $item->render() . ")";
+                    $stack[] = ( $key !== 0 ? "{$item->condition} " : "" ) . "(" . $item->render($skipToken) . ")";
                 }
             }
             else {
@@ -67,7 +67,7 @@ class Where extends Fragment {
         }
 
         return $this->renderSegments([
-            ! $this->parent ? static::SQL_TOKEN : "",
+            ! $this->parent && ! $skipToken ? static::SQL_TOKEN : "",
             implode(" ", $stack)
         ]);
     }
diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php
index c597c79..6bc73e6 100644
--- a/src/QueryBuilder.php
+++ b/src/QueryBuilder.php
@@ -8,6 +8,8 @@ class QueryBuilder
 
     public Query\Having $having;
     
+    public QueryBuilder $parent;
+    
     /**
      * Those are the parameters we are going to bind to PDO.
      */
@@ -18,10 +20,10 @@ class QueryBuilder
      * Those values are to be inserted or updated
      */
     public array $values = [];
-
+    
     public string $whereConditionOperator = Query\Where::CONDITION_AND;
     
-    public string $havingConditionOperator = Query\Having::CONDITION_AND;
+    public string $havingConditionOperator = Query\Where::CONDITION_AND;
     
     protected int $parameterIndex = 0;
 
@@ -194,7 +196,12 @@ class QueryBuilder
         return $this;
     }
 
-    public function having($field, $value, string $operator = Query\Having::OPERATOR_EQUAL, string $condition = Query\Having::CONDITION_AND, bool $not = false) : self
+    public function notWhere($field, $value, string $operator = Query\Where::CONDITION_AND) : self
+    {
+        return $this->where($field, $value, $operator, true);
+    }
+
+    public function having($field, $value, string $operator = Query\Where::OPERATOR_EQUAL, string $condition = Query\Where::CONDITION_AND, bool $not = false) : self
     {
         if ( $this->having ?? false ) {
             $having = $this->having;
@@ -209,15 +216,16 @@ class QueryBuilder
         
         return $this;
     }
-
     
-    public function notWhere($field, $value, string $operator = Query\Where::CONDITION_AND) : self
+    public function groupBy(string $field, ? string $direction = null) : self
     {
-        return $this->where($field, $value, $operator, true);
-    }
+        if ( null === $groupBy = $this->getFragment(Query\GroupBy::class) ) {
+            $groupBy = new Query\GroupBy();
+            $this->push($groupBy);
+        }
 
-    public function groupBy() : self
-    {
+        $groupBy->add($field, $direction);
+        
         return $this;
     }
 
@@ -258,6 +266,13 @@ class QueryBuilder
     }
 
     public function join(string $type, /*string | QueryBuilder*/ $table, $field, $value, bool $outer = false, ? string $alias = null) : self
+    {
+        $this->withJoin(...func_get_args());
+        
+        return $this;
+    }
+
+    public function withJoin(string $type, $table, $field, $value, bool $outer = false, ? string $alias = null) : Query\Join
     {
         $join = new Query\Join($this);
         
@@ -269,17 +284,41 @@ class QueryBuilder
         
         $join->alias = $alias;
         
+        return $join;
+    }
+    
+    public function truncate(string $table, ? string $alias = null, ? string $database = null, ? string $schema = null) : self
+    {
+        if ( $schema ) {
+            $table = "$schema.$table";
+        }
+        
+        if ( $database ) {
+            $table = "$database.$table";
+        }
+
+        $truncate = new Query\Truncate($this);
+        
+        $this->push($truncate);
+
+        $truncate->set($table);
+        
         return $this;
     }
-
+    
     public function push(Query\Fragment $queryFragment) : self
     {
         $this->queryStack[] = $queryFragment;
         
         return $this;
     }
+    
+    public function pull(Query\Fragment $queryFragment) : self
+    {
+        return array_shift($this->queryStack);
+    }
 
-    public function render() : string
+    public function render(bool $skipToken = false) : string
     {
         $sql = [];
 
@@ -288,7 +327,7 @@ class QueryBuilder
         });
 
         foreach($this->queryStack as $fragment) {
-            $sql[] = $fragment->render();
+            $sql[] = $fragment->render($skipToken);
         }
 
         return implode(" ", $sql);
@@ -298,10 +337,10 @@ class QueryBuilder
     {
         $this->parameters = $this->values = $this->queryStack = [];
         $this->whereConditionOperator = Query\Where::CONDITION_AND;
-        $this->havingConditionOperator = Query\Having::CONDITION_AND;
+        $this->havingConditionOperator = Query\Where::CONDITION_AND;
         $this->parameterIndex = 0;
 
-        unset($this->where);
+        unset($this->where, $this->having);
     }
 
     public function getFragment(string $class) : ? Query\Fragment
@@ -331,12 +370,16 @@ class QueryBuilder
 
     public function addParameter($value, string $key = null) : string 
     {
+        if ( $this->parent ?? false ) {
+            return $this->parent->addParameter($value, $key);
+        }
+        
         if ( $key === null ) {
             $key = ":p" . $this->parameterIndex++;
         }
 
-        $this->parameters[$key] = $value;
-        
+        $this->parameters[$key] = $value;    
+
         return $key;
     }
     
diff --git a/src/Repository.php b/src/Repository.php
index d856bf8..b093bbb 100644
--- a/src/Repository.php
+++ b/src/Repository.php
@@ -6,7 +6,7 @@ use Ulmus\Common\EntityResolver;
 
 class Repository
 {
-    use EventTrait;
+    use EventTrait, Repository\ConditionTrait;
     
     const DEFAULT_ALIAS = "this";
 
@@ -27,7 +27,7 @@ class Repository
         $this->alias = $alias;
         $this->entityResolver = Ulmus::resolveEntity($entity);
         $this->adapter = $adapter ?? $this->entityResolver->databaseAdapter() ?? Ulmus::$defaultAdapter;
-        $this->queryBuilder = new QueryBuilder( $this->adapter->adapter() );
+        $this->queryBuilder = new QueryBuilder();
     }
 
     public function loadOne() : ? object
@@ -61,7 +61,14 @@ class Repository
             $this->queryBuilder->removeFragment($select);
         }
         
-        $this->select("COUNT(*)")->selectSqlQuery();
+        if ( $this->queryBuilder->getFragment(Query\GroupBy::class) ) {
+            $this->select( "DISTINCT COUNT(*) OVER ()" );
+        }
+        else {
+            $this->select(Common\Sql::function("COUNT", "*"));
+        }
+        
+        $this->selectSqlQuery();
 
         $this->finalizeQuery();
 
@@ -161,14 +168,35 @@ class Repository
         }
     }
     
+    public function truncate(? string $table = null, ? string $alias = null, ? string $schema = null) : self
+    {
+        $schema = $schema ?: $this->entityResolver->schemaName();
+        
+        $this->queryBuilder->truncate($this->escapeTable($table ?: $this->entityResolver->tableName()), $alias ?: $this->alias, $this->escapeDatabase($this->adapter->adapter()->database), $schema ? $this->escapeSchema($schema) : null);
+        
+        $this->finalizeQuery();
+        
+        $result = Ulmus::runSelectQuery($this->queryBuilder, $this->adapter);
+        
+        return $this;
+    }
+    
     public function generateDatasetDiff(object $entity) : array
     {
         return array_diff_assoc( array_change_key_case($entity->toArray()), array_change_key_case($entity->entityGetDataset(false, true)) );
     }
-    
-    public function yieldAll() : \Generator
+  
+    public function yield() : \Generator
     {
+        $class = $this->entityClass;
+        
+        $this->selectSqlQuery();
+        
+        $this->finalizeQuery();
 
+        foreach(Ulmus::iterateQueryBuilder($this->queryBuilder, $this->adapter) as $entityData) {
+            yield ( new $class() )->entityFillFromDataset($entityData);
+        }
     }
 
     public function select($fields) : self
@@ -220,194 +248,23 @@ class Repository
         return $this;
     }
 
-    public function join(string $type, $table, $field, $value, ? string $alias = null) : self
+    public function join(string $type, $table, $field, $value, ? string $alias = null, ? callable $callback = null) : self
     {
-        $this->queryBuilder->join($type, $this->escapeTable($table), $field, $value, false, $alias);
+        $join = $this->queryBuilder->withJoin($type, $this->escapeTable($table), $field, $value, false, $alias);
         
-        return $this;
-    }
-
-    public function outerJoin(string $type, $table, $field, $value, ? string $alias = null) : self
-    {
-        $this->queryBuilder->join($type, $this->escapeTable($table), $field, $value, true, $alias);
-        
-        return $this;
-    }
-
-    public function open(string $condition = Query\Where::CONDITION_AND) : self
-    {
-        $this->queryBuilder->open($condition);
-        
-        return $this;
-    }
-
-    public function orOpen() : self
-    {
-        return $this->open(Query\Where::CONDITION_OR);
-    }
-
-    public function close() : self
-    {
-        $this->queryBuilder->close();
-        
-        return $this;
-    }
-
-    public function where($field, $value, string $operator = Query\Where::OPERATOR_EQUAL) : self
-    {
-        $this->queryBuilder->where($field, $value, $operator, Query\Where::CONDITION_AND);
-        
-        return $this;
-    }
-
-    public function wheres(array $fieldValues, string $operator = Query\Where::OPERATOR_EQUAL) : self
-    {
-        foreach($fieldValues as $field => $value) {
-            if ( is_array($value) ) {
-                switch ($value[1]) {
-                    case Query\Where::CONDITION_AND:
-                        $this->where($field, $value[0], $operator);
-                    break;
-
-                    case Query\Where::CONDITION_OR:
-                        $this->or($field, $value[0], $operator);
-                    break;
-
-                    case Query\Where::CONDITION_NOT:
-                        $this->notWhere($field, $value[0], $operator);
-                    break;
-                }
-            }
-            else {
-                $this->where($field, $value, $operator);
-            }
+        if ( $callback ) {
+            $callback($join);
         }
         
         return $this;
     }
-    
-    public function and($field, $value, string $operator = Query\Where::OPERATOR_EQUAL) : self
-    {
-        return $this->where($field, $value, $operator);
-    }
 
-    public function or($field, $value, string $operator = Query\Where::OPERATOR_EQUAL) : self
+    public function outerJoin(string $type, $table, $field, $value, ? string $alias = null, ? callable $callback = null) : self
     {
-        $this->queryBuilder->where($field, $value, $operator, Query\Where::CONDITION_OR);
+        $join = $this->queryBuilder->withJoin($type, $this->escapeTable($table), $field, $value, true, $alias);
         
-        return $this;
-    }
-
-    public function notWhere($field, $value, string $operator = Query\Where::OPERATOR_NOT_EQUAL) : self
-    {
-        $this->queryBuilder->where($field, $value, $operator, Query\Where::CONDITION_AND, true);
-        
-        return $this;
-    }
-
-    public function orNot($field, $value, string $operator = Query\Where::OPERATOR_NOT_EQUAL) : self
-    {
-        $this->queryBuilder->notWhere($field, $value, $operator, Query\Where::CONDITION_OR, true);
-        
-        return $this;
-    }
-
-    public function having($field, $value, string $operator = Query\Having::OPERATOR_EQUAL) : self
-    {
-        $this->queryBuilder->having($field, $value, $operator, Query\Having::CONDITION_AND);
-        
-        return $this;
-    }
-
-    public function orHaving($field, $value, string $operator = Query\Where::OPERATOR_EQUAL) : self
-    {
-        $this->queryBuilder->having($field, $value, $operator, Query\Having::CONDITION_OR);
-        
-        return $this;
-    }
-
-    public function notHaving($field, $value, string $operator = Query\Where::OPERATOR_NOT_EQUAL) : self
-    {
-        $this->queryBuilder->having($field, $value, $operator, Query\Having::CONDITION_AND, true);
-        
-        return $this;
-    }
-
-    public function orNotHaving($field, $value) : self
-    {
-        $this->queryBuilder->having($field, $value, Query\Where::OPERATOR_NOT_EQUAL, Query\Having::CONDITION_OR, true);
-        
-        return $this;
-    }
-    
-    public function in($field, $value, string $operator = Query\Where::OPERATOR_EQUAL) : self
-    {
-        $this->queryBuilder->where($field, $value, $operator);
-        
-        return $this;
-    }
-
-    public function orIn($field, $value, string $operator = Query\Where::OPERATOR_EQUAL) : self
-    {
-        $this->queryBuilder->where($field, $value, $operator, Query\Where::CONDITION_OR);
-        
-        return $this;
-    }
-
-    public function notIn($field, $value) : self
-    {
-        $this->queryBuilder->where($field, $value, Query\Where::OPERATOR_NOT_EQUAL);
-        
-        return $this;
-    }
-
-    public function orNotIn($field, $value) : self
-    {
-        return $this->orNot($field, $value, Query\Where::OPERATOR_NOT_EQUAL, Query\Where::CONDITION_OR, true);
-    }
-
-    public function like($field, $value) : self
-    {
-        $this->where($field, $value, Query\Where::OPERATOR_LIKE);
-        
-        return $this;
-    }
-
-    public function orLike($field, $value) : self
-    {
-        $this->or($field, $value, Query\Where::OPERATOR_LIKE);
-        
-        return $this;
-    }
-
-    public function notLike($field, $value) : self
-    {
-        $this->notWhere($field, $value, Query\Where::OPERATOR_LIKE);
-        
-        return $this;
-    }
-
-    public function likes(array $fieldValues, string $condition = Query\Where::CONDITION_AND) : self
-    {
-        foreach($fieldValues as $field => $value) {
-            if ( is_array($value) ) {
-                switch ($value[1]) {
-                    case Query\Where::CONDITION_AND:
-                        $this->like($field, $value[0]);
-                    break;
-
-                    case Query\Where::CONDITION_OR:
-                        $this->orLike($field, $value[0]);
-                    break;
-
-                    case Query\Where::CONDITION_NOT:
-                        $this->notLike($field, $value[0]);
-                    break;
-                }
-            }
-            else {
-                $this->like($field, $value);
-            }
+        if ( $callback ) {
+            $callback($join);
         }
         
         return $this;
@@ -432,15 +289,20 @@ class Repository
     {
 
     }
-
-    public function groupBy() : self
+    
+    public function groupBy($field) : self
     {
+        $this->queryBuilder->groupBy($field);
+        
         return $this;
     }
 
     public function groups(array $groups) : self
     {
-        # foreach($this->groups)
+        foreach($groups as $field ) {
+            $this->groupBy($field);
+        }
+        
         return $this;
     }
     
@@ -454,7 +316,7 @@ class Repository
     public function orders(array $orderList) : self
     {
         foreach($orderList as $field => $direction) {
-            $this->queryBuilder->orderBy($field, $direction);
+            $this->orderBy($field, $direction);
         }
         
         return $this;
@@ -502,7 +364,10 @@ class Repository
     public function withJoin(/*string|array*/ $fields) : self
     {
         $resolvedEntity = Ulmus::resolveEntity($this->entityClass);
-        $this->select("{$this->alias}.*");
+        
+        if ( null === $this->queryBuilder->getFragment(Query\Select::class) ) {
+            $this->select("{$this->alias}.*");
+        }
         
         foreach((array) $fields as $item) {
             if ( null !== $join = $resolvedEntity->searchFieldAnnotation($item, new Annotation\Property\Join) ) {
@@ -528,17 +393,21 @@ class Repository
     
     public function filterServerRequest(SearchRequest\SearchRequestInterface $searchRequest) : self 
     {
-        $searchRequest->count = $searchRequest->filter( clone $this )
-            ->wheres($searchRequest->wheres(), Query\Where::OPERATOR_EQUAL, Query\Where::CONDITION_AND)
-            ->likes($searchRequest->likes(), Query\Where::CONDITION_OR)
-            ->groups($searchRequest->groups())
+        $likes = $searchRequest->likes();
+        $wheres = $searchRequest->wheres();
+        $groups = $searchRequest->groups();
+        
+        $searchRequest->count = $searchRequest->skipCount ? 0 : $searchRequest->filter( clone $this )
+            ->wheres($wheres, Query\Where::OPERATOR_EQUAL, Query\Where::CONDITION_AND)
+            ->likes($likes, Query\Where::CONDITION_OR)
+            ->groups($groups)
             ->count();
         
         return $searchRequest->filter($this)
-            ->wheres($searchRequest->wheres(), Query\Where::OPERATOR_EQUAL, Query\Where::CONDITION_AND)
-            ->likes($searchRequest->likes(), Query\Where::CONDITION_OR)
+            ->wheres($wheres, Query\Where::OPERATOR_EQUAL, Query\Where::CONDITION_AND)
+            ->likes($likes, Query\Where::CONDITION_OR)
             ->orders($searchRequest->orders())
-            ->groups($searchRequest->groups())
+            ->groups($groups)
             ->offset($searchRequest->offset())
             ->limit($searchRequest->limit());
     }
@@ -546,8 +415,8 @@ class Repository
     public function collectionFromQuery(? string $entityClass = null) : EntityCollection
     {
         $class = $entityClass ?: $this->entityClass;
-
-        $entityCollection = $this->instanciateEntityCollection();
+        
+        $entityCollection = $class::entityCollection();
         
         $this->selectSqlQuery();
         
@@ -557,7 +426,7 @@ class Repository
             $entityCollection->append( ( new $class() )->entityFillFromDataset($entityData) );
         }
         
-        $this->eventExecute(Event\RepositoryCollectionFromQueryInterface::class, $entityCollection);
+        $this->eventExecute(Event\Repository\CollectionFromQueryInterface::class, $entityCollection);
 
         return $entityCollection;
     }
diff --git a/src/Repository/ConditionTrait.php b/src/Repository/ConditionTrait.php
new file mode 100644
index 0000000..3c681e0
--- /dev/null
+++ b/src/Repository/ConditionTrait.php
@@ -0,0 +1,153 @@
+<?php
+
+namespace Ulmus\Repository;
+
+use Ulmus\Query;
+
+trait ConditionTrait 
+{
+    public function open(string $condition = Query\Where::CONDITION_AND) : self
+    {
+        $this->queryBuilder->open($condition);
+        
+        return $this;
+    }
+
+    public function orOpen() : self
+    {
+        return $this->open(Query\Where::CONDITION_OR);
+    }
+
+    public function close() : self
+    {
+        $this->queryBuilder->close();
+        
+        return $this;
+    }
+
+    public function where($field, $value, string $operator = Query\Where::OPERATOR_EQUAL) : self
+    {
+        $this->queryBuilder->where($field, $value, $operator, Query\Where::CONDITION_AND);
+        
+        return $this;
+    }
+
+    public function wheres(array $fieldValues, string $operator = Query\Where::OPERATOR_EQUAL) : self
+    {
+        foreach($fieldValues as $field => $value) {
+            $this->where($field, $value, $operator);
+        }
+        
+        return $this;
+    }
+    
+    public function and($field, $value, string $operator = Query\Where::OPERATOR_EQUAL) : self
+    {
+        return $this->where($field, $value, $operator);
+    }
+
+    public function or($field, $value, string $operator = Query\Where::OPERATOR_EQUAL) : self
+    {
+        $this->queryBuilder->where($field, $value, $operator, Query\Where::CONDITION_OR);
+        
+        return $this;
+    }
+
+    public function notWhere($field, $value, string $operator = Query\Where::OPERATOR_NOT_EQUAL) : self
+    {
+        $this->queryBuilder->where($field, $value, $operator, Query\Where::CONDITION_AND, true);
+        
+        return $this;
+    }
+
+    public function orNot($field, $value, string $operator = Query\Where::OPERATOR_NOT_EQUAL) : self
+    {
+        $this->queryBuilder->notWhere($field, $value, $operator, Query\Where::CONDITION_OR, true);
+        
+        return $this;
+    }
+
+    public function having($field, $value, string $operator = Query\Where::OPERATOR_EQUAL) : self
+    {
+        $this->queryBuilder->having($field, $value, $operator, Query\Where::CONDITION_AND);
+        
+        return $this;
+    }
+
+    public function orHaving($field, $value, string $operator = Query\Where::OPERATOR_EQUAL) : self
+    {
+        $this->queryBuilder->having($field, $value, $operator, Query\Where::CONDITION_OR);
+        
+        return $this;
+    }
+
+    public function notHaving($field, $value, string $operator = Query\Where::OPERATOR_NOT_EQUAL) : self
+    {
+        $this->queryBuilder->having($field, $value, $operator, Query\Where::CONDITION_AND, true);
+        
+        return $this;
+    }
+
+    public function orNotHaving($field, $value) : self
+    {
+        $this->queryBuilder->having($field, $value, Query\Where::OPERATOR_NOT_EQUAL, Query\Where::CONDITION_OR, true);
+        
+        return $this;
+    }
+    
+    public function in($field, $value, string $operator = Query\Where::OPERATOR_EQUAL) : self
+    {
+        $this->queryBuilder->where($field, $value, $operator);
+        
+        return $this;
+    }
+
+    public function orIn($field, $value, string $operator = Query\Where::OPERATOR_EQUAL) : self
+    {
+        $this->queryBuilder->where($field, $value, $operator, Query\Where::CONDITION_OR);
+        
+        return $this;
+    }
+
+    public function notIn($field, $value) : self
+    {
+        $this->queryBuilder->where($field, $value, Query\Where::OPERATOR_NOT_EQUAL);
+        
+        return $this;
+    }
+
+    public function orNotIn($field, $value) : self
+    {
+        return $this->orNot($field, $value, Query\Where::OPERATOR_NOT_EQUAL, Query\Where::CONDITION_OR, true);
+    }
+
+    public function like($field, $value) : self
+    {
+        $this->where($field, $value, Query\Where::OPERATOR_LIKE);
+        
+        return $this;
+    }
+
+    public function orLike($field, $value) : self
+    {
+        $this->or($field, $value, Query\Where::OPERATOR_LIKE);
+        
+        return $this;
+    }
+
+    public function notLike($field, $value) : self
+    {
+        $this->notWhere($field, $value, Query\Where::OPERATOR_LIKE);
+        
+        return $this;
+    }
+
+    public function likes(array $fieldValues, string $condition = Query\Where::CONDITION_AND) : self
+    {
+        foreach($fieldValues as $field => $value) {
+            $this->like($field, $value);
+        }
+        
+        return $this;
+    }
+}
\ No newline at end of file
diff --git a/src/SearchRequest/SearchRequestPaginationTrait.php b/src/SearchRequest/SearchRequestPaginationTrait.php
index c4d0de9..0494c34 100644
--- a/src/SearchRequest/SearchRequestPaginationTrait.php
+++ b/src/SearchRequest/SearchRequestPaginationTrait.php
@@ -12,6 +12,8 @@ trait SearchRequestPaginationTrait {
     
     public int $limit = 25;
     
+    public bool $skipCount = false;
+    
     public function limit(): int 
     {
         return $this->limit;