diff --git a/src/Storm/Event/Abstract.php b/src/Storm/Event/Abstract.php
index d25da741915d580396e1045db3238df0d2ba504d..2c9947cd1fb2395d7afa0916c384e39351cd9412 100644
--- a/src/Storm/Event/Abstract.php
+++ b/src/Storm/Event/Abstract.php
@@ -26,24 +26,23 @@ THE SOFTWARE.
 
 
 abstract class Storm_Event_Abstract {
-  protected $_model;
 
-  public function __construct($model) {
-    $this->_model = $model;
+  public function isSaveEvent() {
+    return false;
   }
 
 
-  public function getModel() {
-    return $this->_model;
+  public function isDeleteEvent() {
+    return false;
   }
 
 
-  public function isSaveEvent() {
+  public function isQueryEvent() {
     return false;
   }
 
 
-  public function isDeleteEvent() {
-    return false;
+  public function getModel() {
+    return null;
   }
 }
diff --git a/src/Storm/Event/Delete.php b/src/Storm/Event/Delete.php
index 50e7a606849a6c57ec83152102695db49481ba78..b7cd54fb7198000ff031a0a5570470aceb7a0b57 100644
--- a/src/Storm/Event/Delete.php
+++ b/src/Storm/Event/Delete.php
@@ -25,7 +25,8 @@ THE SOFTWARE.
 */
 
 
-class Storm_Event_Delete extends Storm_Event_Abstract {
+class Storm_Event_Delete extends Storm_Event_Model {
+
   public function isDeleteEvent() {
     return true;
   }
diff --git a/src/Storm/Event/Model.php b/src/Storm/Event/Model.php
new file mode 100644
index 0000000000000000000000000000000000000000..65a3df0997dad99367dc5758c2687942acb6a052
--- /dev/null
+++ b/src/Storm/Event/Model.php
@@ -0,0 +1,40 @@
+<?php
+/*
+STORM is under the MIT License (MIT)
+
+Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+*/
+
+
+class Storm_Event_Model extends Storm_Event_Abstract {
+
+  protected $_model;
+
+  public function __construct($model) {
+    $this->_model = $model;
+  }
+
+
+  public function getModel() {
+    return $this->_model;
+  }
+}
diff --git a/src/Storm/Event/Save.php b/src/Storm/Event/Save.php
index 87a0f413f23184c0452c4dd1ad1023cf0c8b16b7..66b5463878e768c893c1f84bfa41360d374f5e11 100644
--- a/src/Storm/Event/Save.php
+++ b/src/Storm/Event/Save.php
@@ -25,8 +25,9 @@ THE SOFTWARE.
 */
 
 
-class Storm_Event_Save extends Storm_Event_Abstract {
+class Storm_Event_Save extends Storm_Event_Model {
+
   public function isSaveEvent() {
     return true;
   }
-}
\ No newline at end of file
+}
diff --git a/src/Storm/Events.php b/src/Storm/Events.php
index f497336bfc72e62536dc12a2cb6a88011b851be4..f37ba57b84c4d21b1fbe7f9885d1a0834d726676 100644
--- a/src/Storm/Events.php
+++ b/src/Storm/Events.php
@@ -84,10 +84,7 @@ class Storm_Events {
   }
 
 
-  /**
-   * @param $event Storm_Event
-   */
-  public function notify($event) {
+  public function notify(Storm_Event_Abstract $event) : self {
     $this->_observers->eachDo(function($observer) use($event)
                               {
                                 $observer($event);
diff --git a/src/Storm/Join.php b/src/Storm/Join.php
new file mode 100644
index 0000000000000000000000000000000000000000..f814d8b704b18a8bca1b3010508bf5426a1cdfa9
--- /dev/null
+++ b/src/Storm/Join.php
@@ -0,0 +1,75 @@
+<?php
+/*
+  STORM is under the MIT License (MIT)
+
+  Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+  THE SOFTWARE.
+
+*/
+
+
+class Storm_Join extends Storm_Join_Abstract {
+  use Storm_Query_Trait;
+
+  public function __construct(Storm_Model_Loader $loader, ?string $alias = null) {
+    $this->_helper = new Storm_Query_Helper($loader, $alias);
+    $this->_criteria = (new Storm_Query_Criteria)
+      ->setAlias($this->_helper->getIdentifier());
+  }
+
+
+  protected function _prepareHelper() : Storm_Query_Helper {
+    $helper = $this->getHelper();
+    $count = $this->_countSelect();
+    if (0 === $count)
+      $helper->allSelect();
+
+    if ($count > 1)
+      $this->_selectWithAlias();
+
+    $helper->beRow();
+
+    return $helper;
+  }
+
+
+  /** DB management */
+
+  public function getAssembleSelect() : Zend_Db_Table_Select {
+    $select = parent::getAssembleSelect();
+    $this->_assembleGroupBy($select);
+    $this->_assembleOrders($select);
+
+    if ($this->_distinct)
+      $select->distinct();
+
+    return $select;
+  }
+
+
+  /** Volatile management */
+
+  public function computeErrors() : array {
+    foreach ($this->getJoins() as $join)
+      $this->_errors = [...$this->_errors, ...$join->computeErrors()];
+
+    return parent::computeErrors();
+  }
+}
diff --git a/src/Storm/Join/Abstract.php b/src/Storm/Join/Abstract.php
new file mode 100644
index 0000000000000000000000000000000000000000..7ba9ca43c9972ebfd361936f663621e1bcee5c3c
--- /dev/null
+++ b/src/Storm/Join/Abstract.php
@@ -0,0 +1,132 @@
+<?php
+/*
+  STORM is under the MIT License (MIT)
+
+  Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+  THE SOFTWARE.
+
+*/
+
+
+abstract class Storm_Join_Abstract extends Storm_Query_Abstract {
+
+  protected array $_joins = [];
+
+  public function getJoins() : array {
+    return $this->_joins;
+  }
+
+
+  public function inner(Storm_Join_Inner $inner) : self {
+    $this->_joins [] = $inner->setParentHelper($this->getHelper());
+
+    return $this;
+  }
+
+
+  public function left(Storm_Join_Left $left) : self {
+    $this->_joins [] = $left->setParentHelper($this->getHelper());
+
+    return $this;
+  }
+
+
+  public function right(Storm_Join_Right $right) : self {
+    $this->_joins [] = $right->setParentHelper($this->getHelper());
+
+    return $this;
+  }
+
+
+  protected function _countSelect() : int {
+    $count = 0;
+
+    if ($this->getHelper()->hasSelect())
+      $count++;
+
+    foreach ($this->getJoins() as $join)
+      $count += $join->_countSelect();
+
+    return $count;
+  }
+
+
+  protected function _selectWithAlias() : self {
+    $this->getHelper()->beWithAlias();
+
+    foreach ($this->getJoins() as $join)
+      $join->_selectWithAlias();
+
+    return $this;
+  }
+
+
+  /** DB management */
+
+  public function getAssembleSelect() : Zend_Db_Table_Select {
+    $select = parent::getAssembleSelect();
+
+    $this->assembleJoins($select);
+
+    return $select;
+  }
+
+
+  protected function _assembleFrom(Zend_Db_Table_Select $select) : self {
+    $select->setIntegrityCheck(false);
+
+    return parent::_assembleFrom($select);
+  }
+
+
+  public function assembleJoins(Zend_Db_Table_Select $select) : self {
+    $this->_assembleJoin($select);
+
+    foreach ($this->getJoins() as $join)
+      $join->assembleJoins($select);
+
+    return $this;
+  }
+
+
+  protected function _assembleJoin(Zend_Db_Table_Select $select) : self {
+    return $this;
+  }
+
+
+  /** Volatile management */
+
+  public function instances(?array $instances = []) : array {
+    $instances = parent::instances($instances);
+
+    foreach ($this->getJoins() as $join)
+      $instances = $join->instances($instances);
+
+    return $instances;
+  }
+
+
+  public function filterWith(Storm_Query_Instances $query_instances) : self {
+    foreach ($this->getJoins() as $join)
+      $join->filterWith($query_instances);
+
+    return parent::filterWith($query_instances);
+  }
+}
diff --git a/src/Storm/Join/Inner.php b/src/Storm/Join/Inner.php
new file mode 100644
index 0000000000000000000000000000000000000000..a5daf6065e6045093fdae81f9ca62135d10c773d
--- /dev/null
+++ b/src/Storm/Join/Inner.php
@@ -0,0 +1,47 @@
+<?php
+/*
+  STORM is under the MIT License (MIT)
+
+  Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+  THE SOFTWARE.
+
+*/
+
+
+class Storm_Join_Inner extends Storm_Join_Sub {
+
+  public function computeErrors() : array {
+    $errors = [];
+
+    if ( ! $this->_condition)
+      $errors [] = 'Error: For Inner condition "on" is mandatory';
+
+    return [...$errors, ...parent::computeErrors()];
+  }
+
+
+  protected function _assembleJoin(Zend_Db_Table_Select $select) : self {
+    $select->join($this->getHelper()->table(),
+                  $this->_joinConditions(),
+                  $this->getHelper()->select());
+
+    return $this;
+  }
+}
diff --git a/src/Storm/Join/Left.php b/src/Storm/Join/Left.php
new file mode 100644
index 0000000000000000000000000000000000000000..39edebc15838084821b619665b896209b759708d
--- /dev/null
+++ b/src/Storm/Join/Left.php
@@ -0,0 +1,47 @@
+<?php
+/*
+  STORM is under the MIT License (MIT)
+
+  Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+  THE SOFTWARE.
+
+*/
+
+
+class Storm_Join_Left extends Storm_Join_Sub {
+
+  public function computeErrors() : array {
+    $errors = [];
+
+    if ( ! $this->_condition)
+      $errors [] = 'Error: For Left condition "on" is mandatory';
+
+    return [...$errors, ...parent::computeErrors()];
+  }
+
+
+  protected function _assembleJoin(Zend_Db_Table_Select $select) : self {
+    $select->joinLeft($this->getHelper()->table(),
+                      $this->_joinConditions(),
+                      $this->getHelper()->select());
+
+    return $this;
+  }
+}
diff --git a/src/Storm/Join/Right.php b/src/Storm/Join/Right.php
new file mode 100644
index 0000000000000000000000000000000000000000..25bb4515ba932d370a43d2d06d01cba76b19a171
--- /dev/null
+++ b/src/Storm/Join/Right.php
@@ -0,0 +1,47 @@
+<?php
+/*
+  STORM is under the MIT License (MIT)
+
+  Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+  THE SOFTWARE.
+
+*/
+
+
+class Storm_Join_Right extends Storm_Join_Sub {
+
+  public function computeErrors() : array {
+    $errors = [];
+
+    if ( ! $this->_condition)
+      $errors [] = 'Error: For Right condition "on" is mandatory';
+
+    return [...$errors, ...parent::computeErrors()];
+  }
+
+
+  protected function _assembleJoin(Zend_Db_Table_Select $select) : self {
+    $select->joinRight($this->getHelper()->table(),
+                       $this->_joinConditions(),
+                       $this->getHelper()->select());
+
+    return $this;
+  }
+}
diff --git a/src/Storm/Join/Sub.php b/src/Storm/Join/Sub.php
new file mode 100644
index 0000000000000000000000000000000000000000..5d00a9c220ed041896a1430fc6d2737956cf6e0d
--- /dev/null
+++ b/src/Storm/Join/Sub.php
@@ -0,0 +1,142 @@
+<?php
+/*
+  STORM is under the MIT License (MIT)
+
+  Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+  THE SOFTWARE.
+
+*/
+
+
+class Storm_Join_Sub extends Storm_Join_Abstract implements Storm_Query_SubInterface {
+
+  protected ?Storm_Query_Helper $_parent_helper = null;
+  protected ?Storm_Query_Condition $_condition = null;
+
+  public function __construct(Storm_Model_Loader $loader, ?string $alias = null) {
+    $this->_helper = new Storm_Query_Helper($loader, $alias);
+    $this->_criteria = (new Storm_Query_Criteria)
+      ->setAlias($this->_helper->getIdentifier());
+  }
+
+
+  public function setParentHelper(Storm_Query_Helper $parent_helper) : self {
+    $this->_parent_helper = $parent_helper;
+
+    return $this;
+  }
+
+
+  public function getParentHelper() : ?Storm_Query_Helper {
+    return $this->_parent_helper;
+  }
+
+
+  public function on_eq(string $parent_key, string $key) : self {
+    $this->_addCondition(Storm_Query_Condition::newWith($this, $parent_key, $key));
+
+    return $this;
+  }
+
+
+  public function on_not_eq(string $parent_key, string $key) : self {
+    $this->_addCondition(Storm_Query_Condition::newWith($this, $parent_key, $key)
+                         ->beNotEqual());
+
+    return $this;
+  }
+
+
+  public function on_gt(string $parent_key, string $key) : self {
+    $this->_addCondition(Storm_Query_Condition::newWith($this, $parent_key, $key)
+                         ->beGreater());
+
+    return $this;
+  }
+
+
+  public function on_gt_eq(string $parent_key, string $key) : self {
+    $this->_addCondition(Storm_Query_Condition::newWith($this, $parent_key, $key)
+                         ->beGreaterEqual());
+
+    return $this;
+  }
+
+
+  public function on_lt(string $parent_key, string $key) : self {
+    $this->_addCondition(Storm_Query_Condition::newWith($this, $parent_key, $key)
+                         ->beLesser());
+
+    return $this;
+  }
+
+
+  public function on_lt_eq(string $parent_key, string $key) : self {
+    $this->_addCondition(Storm_Query_Condition::newWith($this, $parent_key, $key)
+                         ->beLesserEqual());
+
+    return $this;
+  }
+
+
+  public function filterWith(Storm_Query_Instances $query_instances) : self {
+    if ($this->_condition)
+      $this->_condition->filterWith($query_instances);
+
+    return parent::filterWith($query_instances);
+  }
+
+
+  public function computeErrors() : array {
+    $errors = [];
+
+    foreach ($this->getJoins() as $join)
+      $errors = [...$errors, ...$join->computeErrors()];
+
+    foreach ($this->getSubQueries() as $sub)
+      $errors = [...$errors, ...$sub->computeErrors()];
+
+    return $errors;
+  }
+
+
+  protected function _joinConditions() : string {
+    $join_conditions = $this->_condition
+      ? $this->_condition->sql()
+      : [];
+
+    $join_conditions [] = $this->_sqlWhere();
+
+    return implode($this->_criteria->separator(), array_filter($join_conditions));
+  }
+
+
+  protected function _addCondition(Storm_Query_Condition $condition) : self {
+    if ($this->_condition) {
+      $this->_condition->add($condition);
+
+      return $this;
+    }
+
+    $this->_condition = $condition;
+
+    return $this;
+  }
+}
diff --git a/src/Storm/Model/Abstract.php b/src/Storm/Model/Abstract.php
index c742e97b1eba7c3fe34669fe56d94be4efaf3a92..f1fac3fdd55d91a3515ac362eec7fb6facfc5ea8 100644
--- a/src/Storm/Model/Abstract.php
+++ b/src/Storm/Model/Abstract.php
@@ -1,30 +1,31 @@
 <?php
 /*
-STORM is under the MIT License (MIT)
+  STORM is under the MIT License (MIT)
 
-Copyright (c) 2010-2011 Agence Française Informatique http://www.afi-sa.fr
+  Copyright (c) 2010-2011 Agence Française Informatique http://www.afi-sa.fr
 
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
 
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
 
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+  THE SOFTWARE.
 
 */
 
-abstract class Storm_Model_Abstract {
+abstract class Storm_Model_Abstract implements Storm_Model_Interface {
+
   /**
    * @var array
    */
@@ -130,7 +131,6 @@ abstract class Storm_Model_Abstract {
    */
   protected $_errors = array();
 
-
   /**
    * Default values for attributes of a new instance
    * Should be defined in subclasses like:
@@ -139,7 +139,6 @@ abstract class Storm_Model_Abstract {
    */
   protected $_default_attribute_values = array();
 
-
   /**
    * By default, use autoincrement ids generated by the SGBD. When $_fixed_id = true,
    * does not take id from SGBD.
@@ -147,7 +146,6 @@ abstract class Storm_Model_Abstract {
    */
   protected $_fixed_id = false;
 
-
   /**
    * Generic mean to handle associations (in order to extent and replace has_many and belongs_to)
    * Collection of Storm_Model_Association_Abstract subclasses
@@ -155,35 +153,29 @@ abstract class Storm_Model_Abstract {
    */
   protected $_associations;
 
-
-  /**
-   * @param string $class
-   * @return Storm_Model_Loader
-   */
-  protected static function _buildLoaderFor($class) {
+  protected static function _buildLoaderFor(string $class) : Storm_Model_Loader {
     $reflection = new ReflectionClass($class);
     $class_vars = $reflection->getdefaultProperties();
 
     if (isset($class_vars['_loader_class'])) {
       $loader_class = $class_vars['_loader_class'];
-      return new $loader_class($class);
 
+      return new $loader_class($class);
     }
 
     return new Storm_Model_Loader($class);
 
   }
 
-  public static function getClassVar($var) {
+
+  public static function getClassVar(string $var) {
     $reflection = new ReflectionClass(get_called_class());
     $class_vars = $reflection->getdefaultProperties();
 
     return $class_vars[$var];
   }
 
-  /**
-   * @return Storm_Model_Loader
-   */
+
   public static function getLoader() {
     return self::getLoaderFor(get_called_class());
   }
@@ -203,22 +195,14 @@ abstract class Storm_Model_Abstract {
   }
 
 
-  /**
-   * @param string $class
-   * @return Storm_Model_Loader
-   */
-  public static function getLoaderFor($class) {
+  public static function getLoaderFor(string $class) {
     return isset(self::$_loaders[$class])
       ? self::$_loaders[$class]
       : self::setLoaderFor($class, self::_buildLoaderFor($class));
   }
 
 
-  /**
-   * @param string $class
-   * @param Storm_Model_Loader $loader
-   */
-  public static function setLoaderFor($class, $loader) {
+  public static function setLoaderFor(string $class, $loader) {
     return self::$_loaders[$class] = $loader;
   }
 
@@ -228,7 +212,7 @@ abstract class Storm_Model_Abstract {
   }
 
 
-  public static function getLoaders() {
+  public static function getLoaders() : array {
     return self::$_loaders;
   }
 
@@ -254,9 +238,9 @@ abstract class Storm_Model_Abstract {
    * Put the instance in its Loader's cache
    * @return Storm_Model_Abstract
    */
-
   public function cache() {
     static::getLoader()->cacheInstance($this);
+
     return $this;
   }
 
@@ -267,6 +251,7 @@ abstract class Storm_Model_Abstract {
    */
   public function save($force_primary_key = false) {
     $this->_updateNullBelongsToIdFieldsFromDependents();
+
     if ($valid = $this->isValid()) {
       $this->saveWithoutValidation($force_primary_key);
       $this->_getEvents()->notify(new Storm_Event_Save($this));
@@ -285,10 +270,12 @@ abstract class Storm_Model_Abstract {
   public function assertSave($force_primary_key = false) {
     if (!$this->save($force_primary_key))
       throw new Storm_Model_Exception('Can\'t save '.get_class($this).': '.implode(',',$this->getErrors()));
+
     return true;
   }
 
-  public function getClassName() {
+
+  public function getClassName() : string {
     return get_class($this);
   }
 
@@ -313,23 +300,30 @@ abstract class Storm_Model_Abstract {
 
   protected function _updateNullBelongsToIdFieldsFromDependents() {
     $this->_belongs_to_attributes = array_filter($this->_belongs_to_attributes);
+
     foreach ($this->_belongs_to_attributes as $field => $dependent) {
       $id_field = $this->getIdFieldForDependent($field);
+
       if (null === $this->_get($id_field))
         $this->_set($id_field, $dependent->getId());
     }
+
     return $this;
   }
 
 
   public function beforeSave() {}
 
+
   public function afterSave() {}
 
+
   public function beforeDelete() {}
 
+
   public function afterDelete() {}
 
+
   /**
    * Is this model valid for saving
    *
@@ -357,7 +351,6 @@ abstract class Storm_Model_Abstract {
     $this->validate();
 
     return !$this->hasErrors();
-
   }
 
 
@@ -379,6 +372,7 @@ abstract class Storm_Model_Abstract {
 
   }
 
+
   /**
    * @param string $attribute
    * @param string $error
@@ -401,9 +395,9 @@ abstract class Storm_Model_Abstract {
    * @param string $error
    */
   public function check($condition, $error) {
-    if (!$condition) {
+    if (!$condition)
       $this->addError($error);
-    }
+
     return $this;
   }
 
@@ -414,9 +408,9 @@ abstract class Storm_Model_Abstract {
    * @param string $error
    */
   public function checkAttribute($attribute, $condition, $error) {
-    if (!$condition) {
+    if (!$condition)
       $this->addAttributeError($attribute, $error);
-    }
+
     return $this;
   }
 
@@ -448,10 +442,8 @@ abstract class Storm_Model_Abstract {
    *
    * Used by Loader while saving in order to build the
    * SQL query.
-   *
-   * @return array
    */
-  public function attributesToArray() {
+  public function attributesToArray() : array {
     $attributes = array();
 
     $all_attributes = array_merge($this->_default_attribute_values,
@@ -462,35 +454,22 @@ abstract class Storm_Model_Abstract {
 
       if (method_exists($this, $method)) {
         $attributes[$name] = $this->$method();
-
       } else {
         $attributes[$name] = $value;
       }
     }
 
-
-
     return $attributes;
   }
 
 
-  /**
-   * Return field $_attributes
-   *
-   * @return array
-   */
-  public function getRawAttributes() {
+  public function getRawAttributes() : array {
     return array_merge($this->_default_attribute_values,
                        $this->_attributes);
   }
 
 
-  /**
-   * Return field $_attributes_in_db
-   *
-   * @return array
-   */
-  public function getRawDbAttributes() {
+  public function getRawDbAttributes() : array {
     return $this->_attributes_in_db;
   }
 
@@ -501,10 +480,8 @@ abstract class Storm_Model_Abstract {
    *
    * This method may be redefined in subclasses
    * in order to provide some coupling with Zend_Form::populate()
-   *
-   * @return array
    */
-  public function toArray() {
+  public function toArray() : array {
     return $this->attributesToArray();
   }
 
@@ -536,10 +513,8 @@ abstract class Storm_Model_Abstract {
    * @return Storm_Model_Abstract
    */
   public function setId($id) {
-    if ($this->_table_primary != null) {
+    if ($this->_table_primary != null)
       $this->_set(strtolower($this->_table_primary), $id);
-    }
-
 
     //return $this->_set('id', $id); //perfs
     $this->_attributes['id'] = $id;
@@ -552,29 +527,18 @@ abstract class Storm_Model_Abstract {
    * @return string
    */
   public function getIdFieldForDependent($field) {
-    if (
-      array_key_exists($field, $this->_belongs_to)
-      && (array_key_exists('referenced_in', $this->_belongs_to[$field]))
-    ) {
+    if (array_key_exists($field, $this->_belongs_to)
+        && (array_key_exists('referenced_in', $this->_belongs_to[$field])))
       return $this->_belongs_to[$field]['referenced_in'];
 
-    }
-
     return $field . '_id';
-
   }
 
 
-  /**
-   * @param string $attribute
-   * @return bool
-   */
-  public function isAttributeEmpty($attribute) {
-    if (!array_key_exists($attribute, $this->_attributes)) {
+  public function isAttributeEmpty(string $attribute) : bool {
+    if (!array_key_exists($attribute, $this->_attributes))
       return true;
 
-    }
-
     $value = $this->_get($attribute);
     return empty($value);
   }
@@ -582,13 +546,11 @@ abstract class Storm_Model_Abstract {
 
   protected function _deleteDependents() {
     foreach ($this->hasManyRelationships() as $field => $relation) {
-      if (!array_key_exists('dependents', $relation)) {
+      if (!array_key_exists('dependents', $relation))
         continue;
-      }
 
-      if ($relation['dependents'] != 'delete') {
+      if ($relation['dependents'] != 'delete')
         continue;
-      }
 
       $dependents = $this->_getDependents($field);
 
@@ -605,6 +567,7 @@ abstract class Storm_Model_Abstract {
   protected function _saveDependencies() {
     foreach (array_keys($this->_has_many_attributes) as $field)
       $this->_saveDependents($field);
+
     $this->_associations->save($this);
   }
 
@@ -614,28 +577,22 @@ abstract class Storm_Model_Abstract {
    * @return Storm_Model_Abstract
    */
   protected function _saveDependents($field) {
-    if (array_key_exists('through', $this->descriptionOf($field))) {
+    if (array_key_exists('through', $this->descriptionOf($field)))
       return $this;
-    }
 
-    if (!array_key_exists($field, $this->_has_many_attributes_in_db)) {
+    if (!array_key_exists($field, $this->_has_many_attributes_in_db))
       $this->_has_many_attributes_in_db[$field] = $this->_getDependentsFromLoader($field);
-    }
 
     $has_many_attributes = (array)$this->_has_many_attributes[$field];
     $has_many_attributes_in_db = (array)$this->_has_many_attributes_in_db[$field];
-    $dependents_to_delete = array_diff(
-      $has_many_attributes_in_db,
-      $has_many_attributes
-    );
+    $dependents_to_delete = array_diff($has_many_attributes_in_db,
+                                       $has_many_attributes);
 
-    foreach ($dependents_to_delete as $dependent) {
+    foreach ($dependents_to_delete as $dependent)
       $dependent->delete();
-    }
 
-    foreach($this->_has_many_attributes[$field] as $dependent) {
+    foreach($this->_has_many_attributes[$field] as $dependent)
       $dependent->save();
-    }
 
     $this->_has_many_attributes_in_db[$field] = $this->_has_many_attributes[$field];
     return $this;
@@ -664,12 +621,12 @@ abstract class Storm_Model_Abstract {
   }
 
 
-  protected function _methodMatch($method) {
-    foreach(static::$_method_prefixes as $prefix => $length) {
+  protected function _methodMatch(string $method) : ?array {
+    foreach(static::$_method_prefixes as $prefix => $length)
       if (substr($method, 0, $length) == $prefix)
         return [1 => $prefix,
                 2 => substr($method, $length)];
-    }
+
     return null;
   }
 
@@ -680,16 +637,11 @@ abstract class Storm_Model_Abstract {
    * ex:
    * $car->perform('getColor')
    * $car->perform('setColor', ['red'])
-   *
-   * @param string $method
-   * @param array $args
-   * @return Storm_Model_Abstract
-   * @throws Exception
    */
-  public function perform($method, $args = []) {
-    if (!$matches = $this->_methodMatch($method)) {
-      throw new Storm_Model_Exception('Tried to call unknown method '.get_class($this).'::'.$method);
-    }
+  public function perform(string $method, array $args = []) {
+    if (!$matches = $this->_methodMatch($method))
+      throw new Storm_Model_Exception('Tried to call unknown method '
+                                      . get_class($this) . '::' . $method);
 
     $attribute = $this->_accessorToAttributeName($matches[2]);
 
@@ -716,15 +668,13 @@ abstract class Storm_Model_Abstract {
     }
 
     return $this;
-
   }
 
 
   /**
-   * @param string $field
-   * @return array
+   * @return mixed
    */
-  public function descriptionOf($field) {
+  public function descriptionOf(string $field) {
     if (method_exists($this, $method = 'descriptionOf'.$field))
       return $this->$method();
 
@@ -740,8 +690,10 @@ abstract class Storm_Model_Abstract {
 
   public function hasManyRelationships() {
     $relations = $this->_has_many;
+
     foreach (array_keys($this->_has_many) as $field)
       $relations[$field] = $this->descriptionOf($field);
+
     return $relations;
   }
 
@@ -754,6 +706,7 @@ abstract class Storm_Model_Abstract {
    */
   public function _has($attribute) {
     $dependent = $this->callGetterByAttributeName($attribute);
+
     return !empty($dependent);
   }
 
@@ -802,21 +755,18 @@ abstract class Storm_Model_Abstract {
                                  return $dependent->_numberOf($this->_getDependentFieldNameForInstance($field, $dependent));
                                },
                                $dependents));
-
   }
 
 
   /**
    * Update with a [name] => value formatted array
-   *
-   * @param Array $datas
-   * @return Storm_Model_Abstract
    */
-  public function updateAttributes(Array $datas) {
+  public function updateAttributes(array $datas) : self {
     foreach($datas as $name => $value) {
       $method = 'set'.$this->attributeNameToAccessor($name);
       $this->$method($value);
     }
+
     return $this;
   }
 
@@ -828,13 +778,11 @@ abstract class Storm_Model_Abstract {
    * setAttributeName(...) magic.
    *
    * So we are sure that attributes are exactly those from db.
-   *n
-   * @param Array $datas
-   * @return Storm_Model_Abstract
    */
-  public function initializeAttributes($datas) {
+  public function initializeAttributes(array $datas) : self {
     $this->_attributes_in_db = $this->_attributes = array_merge($this->_attributes,
-                                                              array_change_key_case($datas));
+                                                                array_change_key_case($datas));
+
     return $this;
   }
 
@@ -844,9 +792,9 @@ abstract class Storm_Model_Abstract {
    * @return boolean true when attribute has changed from initial value in db
    */
   public function hasChangedAttribute($name) {
-    if ($this->isNew() ||
-        null === $name ||
-        !isset($this->_attributes[$name]))
+    if ($this->isNew()
+        || null === $name
+        || !isset($this->_attributes[$name]))
       return false;
 
     if (!isset($this->_attributes_in_db[$name]))
@@ -857,45 +805,36 @@ abstract class Storm_Model_Abstract {
 
 
   public function hasChange() {
-    foreach(array_keys($this->toArray()) as $name) {
-      if($this->hasChangedAttribute($name))
-         return true;
-    }
+    foreach(array_keys($this->toArray()) as $name)
+      if ($this->hasChangedAttribute($name))
+        return true;
+
     return false;
   }
 
 
   /**
    * UserId -> user_id
-   *
-   * @param string $accessor
-   * @return string
    */
-  protected function _accessorToAttributeName($accessor) {
+  protected function _accessorToAttributeName(string $accessor) : string {
     return Storm_Inflector::underscorize($accessor);
   }
 
+
   /**
    * user_id -> UserId
-   *
-   * @param string $accessor
-   * @return string
    */
-  public function attributeNameToAccessor($name) {
+  public function attributeNameToAccessor(string $name) : string {
     return Storm_Inflector::camelize($name);
-
   }
 
 
-  /**
-   * @return boolean true if attribute exists
-   */
-  public function isAttributeExists($field) {
+  public function isAttributeExists(string $field) : bool {
     return
-      array_key_exists($field, $this->_attributes) or
-      array_key_exists($field, $this->_has_many) or
-      array_key_exists($field, $this->_belongs_to) or
-      $this->hasDefaultValueForAttribute($field);
+      array_key_exists($field, $this->_attributes)
+      || array_key_exists($field, $this->_has_many)
+      || array_key_exists($field, $this->_belongs_to)
+      || $this->hasDefaultValueForAttribute($field);
   }
 
 
@@ -906,31 +845,25 @@ abstract class Storm_Model_Abstract {
 
   /**
    * Return the value of attribute $field. If cannot find it, raise Exception.
-   * @param string $field
    * @return mixed
    * @throws Storm_Model_Exception
    */
-  protected function _get($field) {
-    if (isset($this->_attributes[$field]) || array_key_exists($field, $this->_attributes)) {
+  protected function _get(string $field) {
+    if (isset($this->_attributes[$field]) || array_key_exists($field, $this->_attributes))
       return $this->_attributes[$field];
-    }
 
-    if (isset($this->_has_many[$field]) || array_key_exists($field, $this->_has_many)) {
+    if (isset($this->_has_many[$field]) || array_key_exists($field, $this->_has_many))
       return $this->_getDependents($field);
-    }
 
-    if (isset($this->_belongs_to[$field]) || array_key_exists($field, $this->_belongs_to)) {
+    if (isset($this->_belongs_to[$field]) || array_key_exists($field, $this->_belongs_to))
       return $this->_getDependent($field);
-    }
 
-    if ($this->hasDefaultValueForAttribute($field)) {
-       return $this->getDefaultValueForAttribute($field);
-    }
+    if ($this->hasDefaultValueForAttribute($field))
+      return $this->getDefaultValueForAttribute($field);
 
-    throw new Storm_Model_Exception(
-                        sprintf('Tried to call unknown method %s::get%s',
-                                get_class($this),
-                                $this->attributeNameToAccessor($field)));
+    throw new Storm_Model_Exception(sprintf('Tried to call unknown method %s::get%s',
+                                            get_class($this),
+                                            $this->attributeNameToAccessor($field)));
   }
 
 
@@ -951,8 +884,8 @@ abstract class Storm_Model_Abstract {
    * @return mixed the value of attribute or null if does not exist.
    */
   public function __get($field) {
-    if (array_key_exists($key = strtolower($field), $this->_attributes) ||
-        array_key_exists($key = strtoupper($field), $this->_attributes))
+    if (array_key_exists($key = strtolower($field), $this->_attributes)
+        || array_key_exists($key = strtoupper($field), $this->_attributes))
       return $this->_attributes[$key];
 
     return null;
@@ -986,12 +919,11 @@ abstract class Storm_Model_Abstract {
    */
   public function __set($field, $value) {
     $method = 'set'.strtolower($field);
-    return $this->$method($value);
 
+    return $this->$method($value);
   }
 
 
-
   /**
    * If has_many relationship specifies unique => true and $value in $dependents,
    * then the constraint is violated.
@@ -1002,17 +934,17 @@ abstract class Storm_Model_Abstract {
    * @return boolean
    */
   protected function _isUniqueConstraintViolated($relationship, $dependents, $value) {
-    $is_unique = (array_key_exists('unique', $relationship) and (true === $relationship['unique']));
+    $is_unique = (array_key_exists('unique', $relationship)
+                  && (true === $relationship['unique']));
     if (!$is_unique)
       return false;
 
     if ($value->isNew())
       return false;
 
-    foreach($dependents as $dependent) {
+    foreach($dependents as $dependent)
       if ($dependent->getId() == $value->getId())
         return true;
-    }
 
     return false;
   }
@@ -1024,16 +956,14 @@ abstract class Storm_Model_Abstract {
    * @return Storm_Model_Abstract
    */
   protected function _addDependent($field, $value) {
-    if (!$value) {
+    if (!$value)
       return $this;
-    }
 
     if (!$map = $this->descriptionOf($field))
       throw new Storm_Model_Exception(
-                          sprintf('Tried to call unknown method %s::add%s',
-                                  get_class($this),
-                                  $this->attributeNameToAccessor($this->_singularize($field))));
-
+                                      sprintf('Tried to call unknown method %s::add%s',
+                                              get_class($this),
+                                              $this->attributeNameToAccessor($this->_singularize($field))));
 
     $dependents = $this->_get($field);
     if ($this->_isUniqueConstraintViolated($map, $dependents, $value))
@@ -1091,6 +1021,7 @@ abstract class Storm_Model_Abstract {
       if ($value->getId() == $other_value->getId())
         return $dependent;
     }
+
     return null;
   }
 
@@ -1130,11 +1061,10 @@ abstract class Storm_Model_Abstract {
 
     $dependents = $this->_get($field);
     $dependents_without_value = array();
-    foreach ($dependents as $dependent) {
-      if ( (!$value->isNew() && ($dependent->getId() != $value->getId())) ||  ($value !== $dependent)) {
+    foreach ($dependents as $dependent)
+      if ( (!$value->isNew() && ($dependent->getId() != $value->getId()))
+          || ($value !== $dependent))
         $dependents_without_value []= $dependent;
-      }
-    }
 
     $this->_set($field, $dependents_without_value);
     return $this;
@@ -1165,11 +1095,9 @@ abstract class Storm_Model_Abstract {
    */
   public function _storeDependentsForField($dependents, $field) {
     // if not already done, set current data in db
-    if (!array_key_exists($field, $this->_has_many_attributes_in_db)) {
+    if (!array_key_exists($field, $this->_has_many_attributes_in_db))
       $this->_has_many_attributes_in_db[$field] = $dependents;
 
-    }
-
     $this->_has_many_attributes[$field] = $dependents;
     return $this;
   }
@@ -1205,7 +1133,6 @@ abstract class Storm_Model_Abstract {
     $getManyMethod = 'get' . ucfirst($field);
     $getOneMethod = 'get' . ucfirst($singularized_field);
 
-
     foreach ($instances as $instance) {
       if ($instance->hasBelongsToRelashionshipWith($singularized_field))
         $dependents []= $instance->$getOneMethod();
@@ -1231,11 +1158,7 @@ abstract class Storm_Model_Abstract {
   }
 
 
-  /**
-   * @param Storm_Model_Abstract $model
-   * @return Storm_Model_Loader
-   */
-  protected function _getLoaderForModel($model) {
+  protected function _getLoaderForModel(string $model) {
     return call_user_func(array($model, 'getLoader'));
   }
 
@@ -1260,10 +1183,9 @@ abstract class Storm_Model_Abstract {
     if (isset($map['limit']))
       $find_params['limit'] = $map['limit'];
 
-    if (isset($map['scope'])) {
+    if (isset($map['scope']))
       foreach($map['scope'] as $scope_field => $scope_value)
         $find_params[$scope_field] = $scope_value;
-    }
 
     $dependents = $this
       ->_getLoaderForModel($map['model'])
@@ -1291,20 +1213,19 @@ abstract class Storm_Model_Abstract {
    * @return Storm_Model_Abstract
    */
   protected function _setDependents($field, $value) {
-    if (!$value) $value = [];
+    if (!$value)
+      $value = [];
 
     $map = $this->descriptionOf($field);
     if (isset($map['through'])) {
       $through_field = $map['through'];
 
-
       if (isset($this->_belongs_to[$through_field])) {
         if ($dependent = $this->_getDependent($through_field))
           return $dependent->callSetterByAttributeName($field, $value);
         return $this;
       }
 
-
       $this->_setDependents($through_field, array());
 
       foreach ($value as $item)
@@ -1313,10 +1234,10 @@ abstract class Storm_Model_Abstract {
       return $this;
     }
 
-
     $this->_has_many_attributes[$field] = $this->_wrapDependentsWithDefinedClass($field, $value);
     foreach($value as $item)
       $item->_set($map['role'], $this);
+
     return $this;
   }
 
@@ -1338,28 +1259,21 @@ abstract class Storm_Model_Abstract {
   }
 
 
-  /**
-   * @param string $attribute
-   */
-  public function callGetterByAttributeName($attribute) {
-    return call_user_func(array($this, 'get'.$this->attributeNameToAccessor($attribute)));
+  public function callGetterByAttributeName(string $attribute) {
+    return call_user_func([$this, 'get' . $this->attributeNameToAccessor($attribute)]);
   }
 
 
   /**
-   * @param string $attribute
    * @param mixed $value
    */
-  public function callSetterByAttributeName($attribute, $value) {
-    return call_user_func([$this, 'set'.$this->attributeNameToAccessor($attribute)], $value);
+  public function callSetterByAttributeName(string $attribute, $value) {
+    return call_user_func([$this, 'set' . $this->attributeNameToAccessor($attribute)], $value);
   }
 
 
-  /**
-   * @param string $attribute
-   */
-  public function callAdderByAttributeName($attribute, $value) {
-    return call_user_func([$this, 'add'.$this->attributeNameToAccessor($this->_singularize($attribute))], $value);
+  public function callAdderByAttributeName(string $attribute, $value) {
+    return call_user_func([$this, 'add' . $this->attributeNameToAccessor($this->_singularize($attribute))], $value);
   }
 
 
@@ -1380,7 +1294,6 @@ abstract class Storm_Model_Abstract {
       return null;
     }
 
-
     $id = $this->_getDependentIdForField($field);
 
     // if the instance is in cache, returns it
@@ -1391,24 +1304,16 @@ abstract class Storm_Model_Abstract {
         return $dependent;
     }
 
-
-    if (null == $id) {
+    if (null == $id)
       return null;
 
-    }
-
     // in runtime cache ?
     if (array_key_exists($field, $this->_belongs_to_attributes)) {
-      if (null == $dependent = $this->_belongs_to_attributes[$field]) {
+      if (null == $dependent = $this->_belongs_to_attributes[$field])
         return null;
 
-      }
-
-      if ($id == $dependent->getId()) {
+      if ($id == $dependent->getId())
         return $dependent;
-
-      }
-
     }
 
     // otherwise in database ?
@@ -1422,33 +1327,25 @@ abstract class Storm_Model_Abstract {
   }
 
 
-  /**
-   * @param string $str
-   * @return string
-   */
-  protected function _singularize($str) {
+  protected function _singularize(string $str) : string {
     return Storm_Inflector::singularize($str);
   }
 
-  /**
-   * @param string $str
-   * @return string
-   */
-  protected function _pluralize($str) {
+
+  protected function _pluralize(string $str) : string {
     return Storm_Inflector::pluralize($str);
   }
 
+
   /**
    * @param string $field
    * @param mixed $value
    * @return Storm_Model_Abstract
    */
   protected function _set($field, $value) {
-    if (array_key_exists($field, $this->_has_many)) {
+    if (array_key_exists($field, $this->_has_many))
       return $this->_setDependents($field, $value);
 
-    }
-
     if (array_key_exists($field, $this->_belongs_to)) {
       $id_field = $this->getIdFieldForDependent($field);
       $this->_attributes[$id_field] = (null == $value) ? null : $value->getId();
@@ -1467,23 +1364,25 @@ abstract class Storm_Model_Abstract {
 
 
   public function acceptHierarchyVisitor($visitor) {
-        $my_name = get_class($this);
+    $my_name = get_class($this);
     foreach ([$this->_belongs_to, $this->_has_many] as $links)
       array_walk(
-                $links,
-          function($link, $link_name) use ($my_name, $visitor) {
-                if (!isset($link['model']))
-                        return;
-            $visitor->visit(self::getLoaderFor($link['model'])->newInstance(),
-                  $my_name, $link_name);
-        });
+                 $links,
+                 function($link, $link_name) use ($my_name, $visitor) {
+                   if (!isset($link['model']))
+                     return;
+                   $visitor->visit(self::getLoaderFor($link['model'])->newInstance(),
+                                   $my_name, $link_name);
+                 });
+
     return $this;
   }
 
 
   public function dotGraph() {
-        $visitor = new Storm_Model_HierarchyVisitor();
+    $visitor = new Storm_Model_HierarchyVisitor();
     $this->acceptHierarchyVisitor($visitor);
+
     return $visitor->asDot();
   }
 
diff --git a/src/Storm/Model/BaseLoader.php b/src/Storm/Model/BaseLoader.php
new file mode 100644
index 0000000000000000000000000000000000000000..10472d5e3d4274ec9d2728ecbd96043e6a33cb21
--- /dev/null
+++ b/src/Storm/Model/BaseLoader.php
@@ -0,0 +1,146 @@
+<?php
+/*
+  STORM is under the MIT License (MIT)
+
+  Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+  THE SOFTWARE.
+
+*/
+
+
+class Storm_Model_BaseLoader {
+
+  protected static $_default_strategy = 'Db';
+  protected string $_model;
+  protected $_persistence_strategy;
+
+  public function __construct(string $class) {
+    $this->_model = $class;
+  }
+
+
+  public function newFromRow(array $row) : Storm_Model_Interface {
+    $row = array_change_key_case($row, CASE_LOWER);
+
+    return $this
+      ->newInstance()
+      ->initializeAttributes($row);
+  }
+
+
+  public function newInstance(?array $attributes = null) : Storm_Model_Interface {
+    $class = $this->_model;
+    $instance = new $class();
+
+    if ( ! empty($attributes))
+      $instance->updateAttributes($attributes);
+
+    return $instance;
+  }
+
+
+  public static function setDefaultStrategy(string $strategy) {
+    static::$_default_strategy = $strategy;
+  }
+
+
+  public static function defaultToVolatile() {
+    static::setDefaultStrategy('Volatile');
+  }
+
+
+  public static function defaultToDb() {
+    static::setDefaultStrategy('Db');
+  }
+
+
+  /**
+   * @param $tbl :  Storm_Model_Table | Storm_Test_ObjectWrapper
+   */
+  public function setTable($tbl) : self {
+    $this->getPersistenceStrategy()->setTable($tbl);
+
+    return $this;
+  }
+
+
+  /**
+   * @return Storm_Model_Table | Storm_Test_ObjectWrapper
+   */
+  public function getTable() {
+    return $this->getPersistenceStrategy()->getTable();
+  }
+
+
+  public function getModel() : string {
+    return $this->_model;
+  }
+
+
+  /**
+   * @return Storm_Model_PersistenceStrategy_Db | Storm_Model_PersistenceStrategy_Volatile
+   */
+  public function getPersistenceStrategy() {
+    if (isset($this->_persistence_strategy))
+      return $this->_persistence_strategy;
+
+    $class_name = 'Storm_Model_PersistenceStrategy_' . static::$_default_strategy;
+    return $this->_persistence_strategy = new $class_name($this);
+  }
+
+
+  public function beVolatile() : self {
+    if ( ! $this->getPersistenceStrategy()->isVolatile())
+      $this->_persistence_strategy = new  Storm_Model_PersistenceStrategy_Volatile($this);
+
+    return $this;
+  }
+
+
+  public function isVolatile() : bool {
+    return $this->getPersistenceStrategy()->isVolatile();
+  }
+
+
+  public function fetchForQuery(Storm_Query_Abstract $query) : array {
+    return $this->getPersistenceStrategy()->fetchForQuery($query);
+  }
+
+
+  public function countForQuery(Storm_Query_Abstract $query) : int {
+    return $this->getPersistenceStrategy()->countForQuery($query);
+  }
+
+
+  public function fetchFirstForQuery(Storm_Query_Abstract $query) : ?Storm_Model_Abstract {
+    return $this->_findFirstFrom($this->fetchForQuery($query->limit(1)));
+  }
+
+
+  protected function _findFirstFrom(array $instances) : ?Storm_Model_Abstract {
+    if (0 === count($instances))
+      return null;
+
+    $first_instance = reset($instances);
+    $this->cacheInstance($first_instance);
+
+    return $first_instance;
+  }
+}
diff --git a/src/Storm/Model/Interface.php b/src/Storm/Model/Interface.php
new file mode 100644
index 0000000000000000000000000000000000000000..4a3115fd97c8144a989cf7e0f735916663980a81
--- /dev/null
+++ b/src/Storm/Model/Interface.php
@@ -0,0 +1,130 @@
+<?php
+/*
+  STORM is under the MIT License (MIT)
+
+  Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+  THE SOFTWARE.
+
+*/
+
+
+interface Storm_Model_Interface {
+
+  /**
+   * Create compatibility with accessor like $miles->instrument.
+   * ex:
+   *    $miles->setInstrument("trumpet");
+   *    assert("trumpet" === $miles->getTrumpet())
+   *    assert("trumpet" === $miles->trumpet)
+   *
+   * If attribute not defined, returns null for compatibility purpose
+   * with legacy code - Remember STORM is intended to get
+   * easily into messy crappy code. Sorry.
+   *
+   *     assert(null ===  $miles->zork)
+   *
+   * @param string $field name of the attribute / field
+   * @return mixed the value of attribute or null if does not exist.
+   */
+  public function __get($field);
+
+
+  public static function getClassVar(string $var);
+
+
+  public static function getLoader();
+
+
+  public static function __callStatic($name, $args);
+
+
+  public static function getLoaderFor(string $class);
+
+
+  public static function setLoaderFor(string $class, $loader);
+
+
+  public static function unsetLoaders();
+
+
+  public static function getLoaders() : array;
+
+
+  public function getClassName() : string;
+
+
+  /**
+   * Return an associative array with attribute
+   * name as $key and its value.
+   *
+   * Used by Loader while saving in order to build the
+   * SQL query.
+   */
+  public function attributesToArray() : array;
+
+
+  public function getRawAttributes() : array;
+
+
+  /**
+   * Return an associative array with attribute
+   * name as $key and its value.
+   *
+   * This method may be redefined in subclasses
+   * in order to provide some coupling with Zend_Form::populate()
+   */
+  public function toArray() : array;
+
+
+  public function isAttributeEmpty(string $attribute) : bool;
+
+
+  /**
+   * See __call
+   *
+   * ex:
+   * $car->perform('getColor')
+   * $car->perform('setColor', ['red'])
+   */
+  public function perform(string $method, array $args = []);
+
+
+  /**
+   * Set initial attribute values with a [name] => value formatted array
+   * Used by Loader::newFromRow
+   * The difference with updateAttributes is that it doesn't call
+   * setAttributeName(...) magic.
+   *
+   * So we are sure that attributes are exactly those from db.
+   */
+  public function initializeAttributes(array $datas) : self;
+
+
+  public function attributeNameToAccessor(string $name) : string;
+
+
+  public function isAttributeExists(string $field) : bool;
+
+
+  public function callGetterByAttributeName(string $attribute);
+
+
+  public function __toString();
+}
diff --git a/src/Storm/Model/Loader.php b/src/Storm/Model/Loader.php
index afda9e42e1dc8512a08045849f4e01deb8c3f882..899bc7fbc78ec91b3c384cb9d651d8b2bec051d9 100644
--- a/src/Storm/Model/Loader.php
+++ b/src/Storm/Model/Loader.php
@@ -1,26 +1,26 @@
 <?php
 /*
-STORM is under the MIT License (MIT)
+  STORM is under the MIT License (MIT)
 
-Copyright (c) 2010-2011 Agence Française Informatique http://www.afi-sa.fr
+  Copyright (c) 2010-2011 Agence Française Informatique http://www.afi-sa.fr
 
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
 
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
 
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+  THE SOFTWARE.
 
 */
 
@@ -42,19 +42,13 @@ THE SOFTWARE.
  *  }
  * }
  */
-class Storm_Model_Loader {
+class Storm_Model_Loader extends Storm_Model_BaseLoader {
+
   /**
    * @var Storm_Model_Loader_Cache
    */
   protected static $_loader_cache;
 
-  protected static $_default_strategy = 'Db';
-
-  /**
-   * @var string
-   */
-  protected $_model;
-
   /**
    * @var Storm_Model_Table
    */
@@ -65,8 +59,6 @@ class Storm_Model_Loader {
    */
   protected $_loaded_instances = array();
 
-  protected $_persistence_strategy;
-
   /**
    * @param $cache Zend_Cache
    */
@@ -75,21 +67,6 @@ class Storm_Model_Loader {
   }
 
 
-  public static function setDefaultStrategy($strategy) {
-    static::$_default_strategy = $strategy;
-  }
-
-
-  public static function defaultToVolatile() {
-    static::setDefaultStrategy('Volatile');
-  }
-
-
-  public static function defaultToDb() {
-    static::setDefaultStrategy('Db');
-  }
-
-
   public static function resetCache() {
     static::$_loader_cache = null;
   }
@@ -102,32 +79,8 @@ class Storm_Model_Loader {
     $this->_model = $class;
     $this->_id_field = strtolower($class::getClassVar('_table_primary'));
 
-    if ($this->_id_field == null) {
+    if ($this->_id_field == null)
       $this->_id_field = 'id';
-    }
-
-  }
-
-  /**
-   * @param Storm_Model_Table $tbl
-   * @return Storm_Model_Loader
-   */
-  public function setTable($tbl) {
-    $this->getPersistenceStrategy()->setTable($tbl);
-    return $this;
-
-  }
-
-  /**
-   * @return Storm_Model_Table
-   */
-  public function getTable() {
-    return $this->getPersistenceStrategy()->getTable();
-  }
-
-
-  public function getModel() {
-    return $this->_model;
   }
 
 
@@ -151,47 +104,25 @@ class Storm_Model_Loader {
   public function getLoaderCache() {
     if (!static::$_loader_cache)
       static::$_loader_cache = new Storm_Model_Loader_NullCache();
-    return static::$_loader_cache;
-  }
-
 
-  public function getPersistenceStrategy() {
-    if (isset($this->_persistence_strategy))
-      return $this->_persistence_strategy;
-
-    // Storm_Model_PersistenceStrategy_Db | Storm_Model_PersistenceStrategy_Volatile
-    $class_name = 'Storm_Model_PersistenceStrategy_' . static::$_default_strategy;
-    return $this->_persistence_strategy = new $class_name($this);
+    return static::$_loader_cache;
   }
 
 
-  public function beVolatile() {
-    if (!$this->getPersistenceStrategy()->isVolatile())
-      $this->_persistence_strategy = new  Storm_Model_PersistenceStrategy_Volatile($this);
-    return $this;
-  }
-
-  public function isVolatile() {
-    return $this->getPersistenceStrategy()->isVolatile();
-  }
-
   /**
    * @param int $id
    * @return Storm_Model_Abstract
    */
   public function find($id) {
-    if(null===$id){
+    if (null === $id)
       return null;
-    }
 
-    if (isset($this->_loaded_instances[$id])) {
+    if (isset($this->_loaded_instances[$id]))
       return $this->_loaded_instances[$id];
-    }
 
     if ($instance = $this->getLoaderCache()->load($this->_model, $id))
       return $instance;
 
-
     if ($instance = $this->getPersistenceStrategy()->find($id)) {
       $this->cacheInstance($instance);
       return $instance;
@@ -210,6 +141,7 @@ class Storm_Model_Loader {
   public function cacheInstance($instance) {
     $this->_loaded_instances[$instance->getId()] = $instance;
     $this->getLoaderCache()->save($instance);
+
     return $this;
   }
 
@@ -224,23 +156,9 @@ class Storm_Model_Loader {
    * @return Storm_Model_Loader
    */
   public function clearCache() {
-    $this->_loaded_instances=[];
-    return $this;
-  }
-
+    $this->_loaded_instances = [];
 
-  /**
-   * @param array $attributes default attributes values
-   * @return a new instance of my model
-   */
-  public function newInstance($attributes = null) {
-    $class = $this->_model;
-    $instance = new $class();
-
-    if (!empty($attributes))
-      $instance->updateAttributes($attributes);
-
-    return $instance;
+    return $this;
   }
 
 
@@ -251,7 +169,7 @@ class Storm_Model_Loader {
    * @return Storm_Model_Abstract
    */
   public function newInstanceWithId($id, $attributes = null) {
-    $instance =  $this
+    $instance = $this
       ->newInstance()
       ->setId($id);
     //id must be set BEFORE other attributes (they may depend on the id's value)
@@ -267,18 +185,17 @@ class Storm_Model_Loader {
   public function newInstanceWithIdAssertSave($id, $attributes = null) {
     $instance = $this->newInstanceWithId($id,$attributes);
     $instance->assertSave(true);
+
     return $instance;
   }
 
+
   /**
    * Create an instance and assigns its
    * attributes using an associative array
    * (name_of_attribute => value)
-   *
-   * @param array $row
-   * @return Storm_Model_Abstract
    */
-  public function newFromRow($row) {
+  public function newFromRow(array $row) : Storm_Model_Interface {
     $row = array_change_key_case($row, CASE_LOWER);
     $id = $row[$this->getIdField()];
     unset($row[$this->getIdField()]);
@@ -291,6 +208,7 @@ class Storm_Model_Loader {
       ->initializeAttributes($row);
   }
 
+
   /**
    * Insert (if new) or update the record in DB
    *
@@ -330,7 +248,7 @@ class Storm_Model_Loader {
   /**
    * @param Array
    */
-  public function deleteBy($clauses, $page_size=100) {
+  public function deleteBy($clauses, $page_size = 100) {
     $done = 0;
     do {
       $clauses['limit'] = $page_size;
@@ -360,8 +278,8 @@ class Storm_Model_Loader {
    */
   public function getIdFieldForDependent($field) {
     $model_instance = new $this->_model;
-    return $model_instance->getIdFieldForDependent($field);
 
+    return $model_instance->getIdFieldForDependent($field);
   }
 
 
@@ -463,19 +381,9 @@ class Storm_Model_Loader {
   }
 
 
-  protected function _findFirstFrom(array $instances) : ?Storm_Model_Abstract {
-    if (0 === count($instances))
-      return null;
-
-    $first_instance = reset($instances);
-    $this->cacheInstance($first_instance);
-
-    return $first_instance;
-  }
-
-
   protected function _prepareFirstBy($args) {
     $args['limit'] = 1;
+
     return $args;
   }
 
@@ -485,17 +393,47 @@ class Storm_Model_Loader {
   }
 
 
-  public function fetchForQuery(Storm_Query $query) : array {
-    return $this->getPersistenceStrategy()->fetchForQuery($query);
+  public function query(?string $alias = null): Storm_Query {
+    return new Storm_Query($this, $alias);
+  }
+
+
+  public function orQuery(?string $alias = null): Storm_Query {
+    return static::query()->beOr();
+  }
+
+
+  public function join(string $alias): Storm_Join {
+    return new Storm_Join($this, $alias);
+  }
+
+
+  public function orJoin(string $alias): Storm_Join {
+    return static::query()->beOr();
+  }
+
+
+  public function inner(string $alias): Storm_Join_Inner {
+    return new Storm_Join_Inner($this, $alias);
+  }
+
+
+  public function left(string $alias): Storm_Join_Left {
+    return new Storm_Join_Left($this, $alias);
+  }
+
+
+  public function right(string $alias): Storm_Join_Right {
+    return new Storm_Join_Right($this, $alias);
   }
 
 
-  public function countForQuery(Storm_Query $query) : int {
-    return $this->getPersistenceStrategy()->countForQuery($query);
+  public function subIn(?string $alias = null): Storm_Query_In {
+    return new Storm_Query_In($this, $alias);
   }
 
 
-  public function fetchFirstForQuery(Storm_Query $query) : ?Storm_Model_Abstract {
-    return $this->_findFirstFrom($this->fetchForQuery($query->limit(1)));
+  public function subExists(?string $alias = null): Storm_Query_Exists {
+    return new Storm_Query_Exists($this, $alias);
   }
 }
diff --git a/src/Storm/Model/PersistenceStrategy/Abstract.php b/src/Storm/Model/PersistenceStrategy/Abstract.php
index e0d0395f876d1bf51c6558e514a01e4e85ffa912..28d4383d46e9d1835d952da950fdbc4de6d82a50 100644
--- a/src/Storm/Model/PersistenceStrategy/Abstract.php
+++ b/src/Storm/Model/PersistenceStrategy/Abstract.php
@@ -27,9 +27,10 @@ THE SOFTWARE.
 
 class Storm_Model_PersistenceStrategy_Abstract  {
 
-  protected Storm_Model_Loader $_loader;
+  protected Storm_Model_BaseLoader $_loader;
+  protected $_table;
 
-  public function __construct(Storm_Model_Loader $loader) {
+  public function __construct(Storm_Model_BaseLoader $loader) {
     $this->_loader = $loader;
   }
 
@@ -47,4 +48,22 @@ class Storm_Model_PersistenceStrategy_Abstract  {
   public function updateAll(array $where, array $set_values) : int {
     return 0;
   }
+
+
+  public function getTable() {
+    if (!isset($this->_table)) {
+      $table_name = call_user_func_array([$this->_loader->getModel(),
+                                          'getClassVar'],
+                                         ['_table_name']);
+      $this->_table = new Storm_Model_Table(['name' => $table_name]);
+    }
+
+    return $this->_table;
+  }
+
+
+  public function setTable($tbl) {
+    $this->_table = $tbl;
+    return $this;
+  }
 }
diff --git a/src/Storm/Model/PersistenceStrategy/Db.php b/src/Storm/Model/PersistenceStrategy/Db.php
index bbc88d469e18b5689b0740e41e7b160673eb9c80..5fbf125fc867fbd6846f801f1472b0b5bc62fe54 100644
--- a/src/Storm/Model/PersistenceStrategy/Db.php
+++ b/src/Storm/Model/PersistenceStrategy/Db.php
@@ -28,29 +28,11 @@ THE SOFTWARE.
 class Storm_Model_PersistenceStrategy_Db
   extends Storm_Model_PersistenceStrategy_Abstract {
 
-  protected $_table;
-
   public function newFromRow($row) {
     return $this->_loader->newFromRow($row);
   }
 
 
-  public function getTable() {
-    if (!isset($this->_table)) {
-      $table_name = call_user_func_array([$this->_loader->getModel(),'getClassVar'],['_table_name']);
-      $this->_table = new Storm_Model_Table(array('name' => $table_name));
-    }
-
-    return $this->_table;
-  }
-
-
-  public function setTable($tbl) {
-    $this->_table = $tbl;
-    return $this;
-  }
-
-
   public function find($id) {
     $rowset = $this->getTable()->find($id)->toArray();
 
@@ -103,7 +85,7 @@ class Storm_Model_PersistenceStrategy_Db
       ? $value
       : Storm_Query_Clause::newWith($key, $value);
 
-    return $clause->getFormatDb($this->getTable());
+    return $clause->getFormatDb();
   }
 
 
@@ -113,19 +95,19 @@ class Storm_Model_PersistenceStrategy_Db
   }
 
 
-  public function fetchForQuery(Storm_Query $query) : array {
+  public function fetchForQuery(Storm_Query_Abstract $query) : array {
     return $this->findAll($this->_assembleQuery($query));
   }
 
 
-  public function countForQuery(Storm_Query $query) : int {
+  public function countForQuery(Storm_Query_Abstract $query) : int {
     return $this->_count($this->_assembleQuery($query));
   }
 
 
-  protected function _assembleQuery(Storm_Query $query) {
-    $select = $this->getTable()->select();
-    $query->assemble($select);
+  protected function _assembleQuery(Storm_Query_Abstract $query) : Zend_Db_Table_Select {
+    $select = $query->getAssembleSelect();
+    $this->setTable($select->getTable());
 
     return $select;
   }
diff --git a/src/Storm/Model/PersistenceStrategy/Volatile.php b/src/Storm/Model/PersistenceStrategy/Volatile.php
index 260c04e95272dfb8c0de6d140bf29428332f613e..680dfb93fa4f9e10db975efabd91450abb2fb5ab 100644
--- a/src/Storm/Model/PersistenceStrategy/Volatile.php
+++ b/src/Storm/Model/PersistenceStrategy/Volatile.php
@@ -27,18 +27,10 @@
 class Storm_Model_PersistenceStrategy_Volatile
   extends Storm_Model_PersistenceStrategy_Abstract {
 
-  protected
-    $_instances = [],
-    $desc_order = false,
-    $_table;
+  protected array $_instances = [];
 
-  public function getTable() {
-    return $this->_table;
-  }
-
-
-  public function setTable($tbl) {
-    $this->_table = $tbl;
+  public function setInstances(array $instances) : self {
+    $this->_instances = $instances;
     return $this;
   }
 
@@ -56,7 +48,7 @@ class Storm_Model_PersistenceStrategy_Volatile
   /**
    * @param $select null | string | array
    */
-  protected function _findAll($select, ?Storm_Query $query) : array {
+  protected function _findAll($select, ?Storm_Query_Abstract $query) : array {
     $group_by = $this->_getGroupBy($select, $query);
     $order = $this->_getOrder($select);
     $limit = $this->_getLimit($select, $query);
@@ -73,7 +65,7 @@ class Storm_Model_PersistenceStrategy_Volatile
     $this->_allMatchingInstancesDo($select,
                                    $query,
                                    function($model) use (&$values) {
-                                     $values []= $model;
+                                     $values [] = $model;
                                    });
 
     $values = $this->groupBy($values, $group_by);
@@ -85,6 +77,9 @@ class Storm_Model_PersistenceStrategy_Volatile
     if ($page_size > 0)
       $values = array_slice($values, $page * $page_size, $page_size);
 
+    if ($query)
+      $query->notify();
+
     return array_map([$this->_loader, 'newFromRow'],
                      $values);
   }
@@ -119,12 +114,17 @@ class Storm_Model_PersistenceStrategy_Volatile
   }
 
 
-  public function fetchForQuery(Storm_Query $query) : array {
+  public function getInstances() : array {
+    return $this->_instances;
+  }
+
+
+  public function fetchForQuery(Storm_Query_Abstract $query) : array {
     return $this->_findAll([], $query);
   }
 
 
-  public function countForQuery(Storm_Query $query) : int {
+  public function countForQuery(Storm_Query_Abstract $query) : int {
     return sizeof($this->fetchForQuery($query));
   }
 
@@ -306,31 +306,25 @@ class Storm_Model_PersistenceStrategy_Volatile
                                           null,
                                           function($model) {
                                             unset($this->_instances[$model['id']]);
-                                          },
-                                          $this->getInstancesArray());
+                                          });
   }
 
 
   protected function _allMatchingInstancesDo(array $clauses,
-                                             ?Storm_Query $query,
+                                             ?Storm_Query_Abstract $query,
                                              callable $callback) : int {
-    $filter_callback = $query
-      ? fn($model) => $this->_modelMatchesQuery($model, $query)
-      : fn($model) => $this->_modelMatchesClauses($model, $clauses);
-
-    $matching_instances = array_filter($this->getInstancesArray(), $filter_callback);
+    $matching_instances = $query
+      ? $query->getMatchingInstances()
+      : $this->_modelsFromClauses($clauses);
 
     return count(array_map($callback, $matching_instances));
   }
 
 
-  protected function _modelMatchesClauses(array $model, array $clauses) : bool {
-    return $this->containsAllAttributes($model, $clauses);
-  }
-
-
-  protected function _modelMatchesQuery(array $model, Storm_Query $query) : bool {
-    return $query->containsAllAttributes($model);
+  protected function _modelsFromClauses(array $clauses) : array {
+    return
+      array_filter($this->getInstancesArray(),
+                   fn($model) => $this->containsAllAttributes($model, $clauses));
   }
 
 
@@ -344,7 +338,7 @@ class Storm_Model_PersistenceStrategy_Volatile
   }
 
 
-  protected function _getLimit(array &$select, ?Storm_Query $query) : string {
+  protected function _getLimit(array &$select, ?Storm_Query_Abstract $query) : string {
     if ($query)
       return $query->getLimitValue();
 
@@ -355,7 +349,7 @@ class Storm_Model_PersistenceStrategy_Volatile
   }
 
 
-  protected function _getLimitPage(array &$select, ?Storm_Query $query) : array {
+  protected function _getLimitPage(array &$select, ?Storm_Query_Abstract $query) : array {
     if ($query)
       return $query->getLimitPageValue();
 
@@ -373,7 +367,7 @@ class Storm_Model_PersistenceStrategy_Volatile
   }
 
 
-  protected function _getGroupBy(array &$select, ?Storm_Query $query) : string {
+  protected function _getGroupBy(array &$select, ?Storm_Query_Abstract $query) : string {
     if ($query)
       return $query->getGroupByValue();
 
diff --git a/src/Storm/Model/Row.php b/src/Storm/Model/Row.php
new file mode 100644
index 0000000000000000000000000000000000000000..70a479a15c5a6526fdf5e12f76d383baf50ac72e
--- /dev/null
+++ b/src/Storm/Model/Row.php
@@ -0,0 +1,245 @@
+<?php
+/*
+  STORM is under the MIT License (MIT)
+
+  Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+  THE SOFTWARE.
+
+*/
+
+
+class Storm_Model_Row implements Storm_Model_Interface {
+
+  const GETTER_METHOD = 'get';
+
+  protected static array $_loaders = [];
+  protected array $_attributes = [];
+  protected string $_table_primary = '';
+  protected Storm_Model_Associations $_associations;
+
+  public function __construct() {
+    $this->_associations = new Storm_Model_Associations;
+  }
+
+
+  /**
+   * Main purpose is to setup generic getters and setters:
+   *
+   * $car->getColor();
+   *
+   * @param string $method
+   * @param array $args
+   * @return Storm_Model_Abstract
+   * @throws Exception
+   */
+  public function __call($method, $args) {
+    return $this->_associations->handleCall($this, $method, $args);
+  }
+
+
+  /**
+   * Create compatibility with accessor like $miles->instrument.
+   * ex:
+   *    $miles->setInstrument("trumpet");
+   *    assert("trumpet" === $miles->getTrumpet())
+   *    assert("trumpet" === $miles->trumpet)
+   *
+   * If attribute not defined, returns null for compatibility purpose
+   * with legacy code - Remember STORM is intended to get
+   * easily into messy crappy code. Sorry.
+   *
+   *     assert(null ===  $miles->zork)
+   *
+   * @param string $field name of the attribute / field
+   * @return mixed the value of attribute or null if does not exist.
+   */
+  public function __get($field) {
+    if (array_key_exists($key = strtolower($field), $this->_attributes)
+        || array_key_exists($key = strtoupper($field), $this->_attributes))
+      return $this->_attributes[$key];
+
+    return null;
+  }
+
+
+  /**
+   * Return the value of attribute $field. If cannot find it, raise Exception.
+   * @return mixed
+   * @throws Storm_Model_Exception
+   */
+  protected function _get(string $field) {
+    if (array_key_exists($field, $this->_attributes))
+      return $this->_attributes[$field];
+
+    throw new Storm_Model_Exception(sprintf('Tried to call unknown method %s::get%s',
+                                            get_class($this),
+                                            $this->attributeNameToAccessor($field)));
+  }
+
+
+  public static function getClassVar(string $var) {
+    $reflection = new ReflectionClass(get_called_class());
+    $class_vars = $reflection->getdefaultProperties();
+
+    return $class_vars[$var];
+  }
+
+
+  public static function getLoader() {
+    return static::getLoaderFor(get_called_class());
+  }
+
+
+  public static function __callStatic($name, $args) {
+    return call_user_func_array([static::getLoader(), $name], $args);
+  }
+
+
+  public static function getLoaderFor(string $class) {
+    return static::$_loaders[$class]
+      ?? static::setLoaderFor($class, (new Storm_Model_BaseLoader($class)));
+  }
+
+
+  public static function setLoaderFor(string $class, $loader) {
+    return static::$_loaders[$class] = $loader;
+  }
+
+
+  public static function unsetLoaders() {
+    static::$_loaders = [];
+  }
+
+
+  public static function getLoaders() : array {
+    return static::$_loaders;
+  }
+
+
+  public function getClassName() : string {
+    return get_class($this);
+  }
+
+
+  /**
+   * Return an associative array with attribute
+   * name as $key and its value.
+   *
+   * Used by Loader while saving in order to build the
+   * SQL query.
+   */
+  public function attributesToArray() : array {
+    return $this->_attributes;
+  }
+
+
+  public function getRawAttributes() : array {
+    return $this->_attributes;
+  }
+
+
+  /**
+   * Return an associative array with attribute
+   * name as $key and its value.
+   *
+   * This method may be redefined in subclasses
+   * in order to provide some coupling with Zend_Form::populate()
+   */
+  public function toArray() : array {
+    return $this->attributesToArray();
+  }
+
+
+  public function isAttributeEmpty(string $attribute) : bool {
+    if ( ! array_key_exists($attribute, $this->_attributes))
+      return true;
+
+    return empty($this->_get($attribute));
+  }
+
+
+  /**
+   * See __call
+   *
+   * ex:
+   * $car->perform('getColor')
+   */
+  public function perform(string $method, array $args = []) {
+    if ( ! $matches = $this->_methodMatch($method))
+      throw new Storm_Model_Exception('Tried to call unknown method '
+                                      . get_class($this) . '::' . $method);
+
+    return static::GETTER_METHOD === $matches[1]
+      ? $this->_get($this->_accessorToAttributeName($matches[2]))
+      : $this;
+  }
+
+
+  protected function _methodMatch(string $method) : ?array {
+    return static::GETTER_METHOD === substr($method, 0, 3)
+      ? [1 => static::GETTER_METHOD,
+         2 => substr($method, 3)]
+      : null;
+  }
+
+
+  /**
+   * UserId -> user_id
+   */
+  protected function _accessorToAttributeName(string $accessor) : string {
+    return Storm_Inflector::underscorize($accessor);
+  }
+
+
+  /**
+   * Set initial attribute values with a [name] => value formatted array
+   * Used by Loader::newFromRow
+   * The difference with updateAttributes is that it doesn't call
+   * setAttributeName(...) magic.
+   *
+   * So we are sure that attributes are exactly those from db.
+   */
+  public function initializeAttributes(array $datas) : self {
+    $this->_attributes = array_merge($this->_attributes, array_change_key_case($datas));
+
+    return $this;
+  }
+
+
+  public function attributeNameToAccessor(string $name) : string {
+    return Storm_Inflector::camelize($name);
+  }
+
+
+  public function isAttributeExists(string $field) : bool {
+    return array_key_exists($field, $this->_attributes);
+  }
+
+
+  public function callGetterByAttributeName(string $attribute) {
+    return call_user_func([$this, static::GETTER_METHOD
+                           . $this->attributeNameToAccessor($attribute)]);
+  }
+
+
+  public function __toString() {
+    return get_class($this);
+  }
+}
diff --git a/src/Storm/Model/Table.php b/src/Storm/Model/Table.php
index 2778a62d1caeb014266a3e70f1786fa8140a5654..b07b1698cc580d865ac5010c69e3cc53195bc570 100644
--- a/src/Storm/Model/Table.php
+++ b/src/Storm/Model/Table.php
@@ -26,6 +26,7 @@ THE SOFTWARE.
 
 class Storm_Model_Table extends Zend_Db_Table_Abstract {
 
+  public function getName() : string {
+    return $this->_name;
+  }
 }
-
-?>
diff --git a/src/Storm/Query.php b/src/Storm/Query.php
index bac1b191e5df2745da62837fab5d5bc541a56ab7..b68e80e03b16bcf51d940b8d674797fdb1d36726 100644
--- a/src/Storm/Query.php
+++ b/src/Storm/Query.php
@@ -25,336 +25,38 @@
 */
 
 
-class Storm_Query implements Storm_Query_CriteriaInterface {
+class Storm_Query extends Storm_Query_Abstract {
+  use Storm_Query_Trait;
 
-  protected array $_orders;
-  protected Storm_Model_Loader $_loader;
-  protected Storm_Query_CriteriaInterface $_criteria;
-  protected ?Storm_Query_Clause $_clause_limit;
-  protected ?Storm_Query_Clause $_clause_limit_page;
-  protected ?Storm_Query_Clause $_clause_group_by;
-
-  protected function __construct(Storm_Model_Loader $loader) {
-    $this->_loader = $loader;
-    $this->_orders = [];
-    $this->_clause_limit = null;
-    $this->_clause_limit_page = null;
-    $this->_clause_group_by = null;
-    $this->_criteria = new Storm_Query_Criteria;
-  }
-
-
-  public static function from(string $loader_class) : self {
-    return new static($loader_class::getLoader());
-  }
-
-
-  /**
-   * @param $limit int | string
-   */
-  public function limit($limit) : self {
-    $this->_clause_limit = Storm_Query_Clause::limit($limit);
-    return $this;
-  }
-
-
-  /**
-   * @param $range array | int | string
-   */
-  public function limit_page($range) : self {
-    $this->_clause_limit_page = Storm_Query_Clause::limitPage($range);
-    return $this;
-  }
-
-
-  public function group(string $value) : self {
-    $this->_clause_group_by = Storm_Query_Clause::group($value);
-    return $this;
-  }
-
-
-  /**
-   * @param $key_or_clause string|Storm_Query_MatchRating
-   */
-  public function order($key_or_clause) : self {
-    $this->_orders [] = $this->_order($key_or_clause)
-      ->setOrder(false);
-    return $this;
+  public function __construct(Storm_Model_Loader $loader, ?string $alias = null) {
+    $this->_helper = new Storm_Query_Helper($loader, $alias);
+    $this->_criteria = (new Storm_Query_Criteria)
+      ->setAlias($this->_helper->getIdentifier());
   }
 
 
-  /**
-   * @param $key_or_clause string|Storm_Query_MatchRating
-   */
-  public function order_desc($key_or_clause) : self {
-    $this->_orders [] = $this->_order($key_or_clause)
-      ->setOrder(true);
-    return $this;
-  }
-
-
-  /**
-   * @param $key_or_clause string|Storm_Query_MatchRating
-   */
-  protected function _order($key_or_clause) : Storm_Query_Clause {
-    if ($key_or_clause instanceof Storm_Query_MatchRating)
-      return Storm_Query_Clause::order($key_or_clause->getKey(),
-                                       Storm_Query_Clause::match($key_or_clause));
-
-    return Storm_Query_Clause::order($key_or_clause);
-  }
-
-
-  public function fetchAll() : array {
-    return $this->_loader->fetchForQuery($this);
-  }
+  protected function _prepareHelper() : Storm_Query_Helper {
+    $helper = $this->getHelper();
+    if ($helper->hasSelect())
+      $helper->beRow();
 
+    if (!$helper->hasSelect())
+      $helper->allSelect();
 
-  public function count() : int {
-    return $this->_loader->countForQuery($this);
-  }
-
-
-  public function fetchFirst() : ?Storm_Model_Abstract {
-    return $this->_loader->fetchFirstForQuery($this);
-  }
-
-
-  /** interface Storm_Query_CriteriaInterface */
-
-  public function beOr() : self {
-    $this->_criteria->beOr();
-    return $this;
-  }
-
-
-  public function or(Storm_Query_CriteriaInterface $criteria) : self {
-    $this->_criteria->or($criteria);
-    return $this;
-  }
-
-
-  public function and(Storm_Query_CriteriaInterface $criteria) : self {
-    $this->_criteria->and($criteria);
-    return $this;
-  }
-
-
-  /**
-   * @param $value int | string
-   */
-  public function eq(string $key, $value) : self {
-    $this->_criteria->eq($key, $value);
-    return $this;
-  }
-
-
-  /**
-   * @param $value int | string
-   */
-  public function not_eq(string $key, $value) : self {
-    $this->_criteria->not_eq($key, $value);
-    return $this;
-  }
-
-
-  public function like(string $key, string $value) : self {
-    $this->_criteria->like($key, $value);
-    return $this;
-  }
-
-
-  public function not_like(string $key, string $value) : self {
-    $this->_criteria->not_like($key, $value);
-    return $this;
-  }
-
-
-  /**
-   * @param $value int | string
-   */
-  public function gt(string $key, $value) : self {
-    $this->_criteria->gt($key, $value);
-    return $this;
-  }
-
-
-  /**
-   * @param $value int | string
-   */
-  public function gt_eq(string $key, $value) : self {
-    $this->_criteria->gt_eq($key, $value);
-    return $this;
-  }
-
-
-  /**
-   * @param $value int | string
-   */
-  public function lt(string $key, $value) : self {
-    $this->_criteria->lt($key, $value);
-    return $this;
-  }
-
-
-  /**
-   * @param $value int | string
-   */
-  public function lt_eq(string $key, $value) : self {
-    $this->_criteria->lt_eq($key, $value);
-    return $this;
-  }
-
-
-  public function is_null(string $key) : self {
-    $this->_criteria->is_null($key);
-    return $this;
-  }
-
-
-  public function not_is_null(string $key) : self {
-    $this->_criteria->not_is_null($key);
-    return $this;
-  }
-
-
-  public function in(string $key, array $array) : self {
-    $this->_criteria->in($key, $array);
-    return $this;
-  }
-
-
-  public function not_in(string $key, array $array) : self {
-    $this->_criteria->not_in($key, $array);
-    return $this;
-  }
-
-
-  public function start(string $key, string $value) : self {
-    $this->_criteria->start($key, $value);
-    return $this;
-  }
-
-
-  public function end(string $key, string $value) : self {
-    $this->_criteria->end($key, $value);
-    return $this;
-  }
-
-
-  public function match(Storm_Query_MatchBoolean $match) : self {
-    $this->_criteria->match($match);
-    return $this;
+    return $helper;
   }
 
 
   /** DB management */
 
-  /**
-   * @param $select Zend_Db_Table_Select|Storm_Test_ObjectWrapper
-   */
-  public function assemble(object $select) : self {
-    $sql = new Storm_Query_Sql;
-    $this->_criteria->assemble($sql);
-    if ($where = $sql->implode($this->_criteria->separator()))
-      $select->where($where);
-
-    foreach ($this->_orders as $order)
-      $order->assemble($select);
-
-    if ($this->_clause_limit)
-      $this->_clause_limit->assemble($select);
-
-    if ($this->_clause_limit_page)
-      $this->_clause_limit_page->assemble($select);
-
-    if ($this->_clause_group_by)
-      $this->_clause_group_by->assemble($select);
-
-    return $this;
-  }
-
-
-  /** Volatile management */
-
-  public function orderVolatile(array $models) : array {
-    return (new Storm_Query_Order(array_reverse($this->_orders)))
-      ->compareVolatile($models);
-  }
-
-
-  public function containsAllAttributes(array $model) : bool {
-    return $this->_criteria->containsAllAttributes($model);
-  }
-
-
-  public function getLimitValue() : string {
-    return $this->_clause_limit
-      ? $this->_clause_limit->getValue()
-      : '';
-  }
-
-
-  public function getLimitPageValue() : array {
-    return $this->_clause_limit_page
-      ? $this->_clause_limit_page->getValue()
-      : [];
-  }
-
-
-  public function getGroupByValue() : string {
-    return $this->_clause_group_by
-      ? $this->_clause_group_by->getValue()
-      : '';
-  }
-}
-
-
-
-
-class Storm_Query_Order {
-
-  protected ?Storm_Query_Order $_next_query;
-  protected ?Storm_Query_Clause $_clause;
-  protected int $_position;
-
-  public function __construct(array $orders, ?int $position = 0) {
-    $this->_position = $position++;
-    $this->_clause = array_shift($orders);
-
-    $this->_next_query = ($orders
-                          ? (new Storm_Query_Order($orders, $position))
-                          : null);
-  }
-
-
-  public function getNextQuery() : ?Storm_Query_Order {
-    if (!($next_query = $this->_next_query))
-      return null;
-
-    return $next_query->getClause()
-      ? $next_query
-      : null;
-  }
-
-
-  public function getClause() : ?Storm_Query_Clause {
-    return $this->_clause;
-  }
-
-
-  public function getPosition() : int {
-    return 10 ** $this->_position;
-  }
-
-
-  public function compareVolatile(array $models) : array {
-    if (!$models)
-      return $models;
+  public function getAssembleSelect() : Zend_Db_Table_Select {
+    $select = parent::getAssembleSelect();
+    $this->_assembleGroupBy($select);
+    $this->_assembleOrders($select);
 
-    if ($clause = $this->getClause())
-      usort($models, fn($a, $b) => $clause->compare($this, $a, $b));
+    if ($this->_distinct)
+      $select->distinct();
 
-    return $models;
+    return $select;
   }
 }
diff --git a/src/Storm/Query/Abstract.php b/src/Storm/Query/Abstract.php
new file mode 100644
index 0000000000000000000000000000000000000000..10051642e56482b096191f79be2354a92363e5de
--- /dev/null
+++ b/src/Storm/Query/Abstract.php
@@ -0,0 +1,516 @@
+<?php
+/*
+  STORM is under the MIT License (MIT)
+
+  Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+  THE SOFTWARE.
+
+*/
+
+
+abstract class Storm_Query_Abstract implements Storm_Query_CriteriaInterface {
+
+  protected array $_errors = [];
+  protected array $_sub_queries = [];
+  protected Storm_Query_Helper $_helper;
+  protected Storm_Query_CriteriaInterface $_criteria;
+  protected ?Storm_Query_Clause $_clause_limit = null;
+  protected ?Storm_Query_Clause $_clause_limit_page = null;
+
+  public function getCriteria() : Storm_Query_CriteriaInterface {
+    return $this->_criteria;
+  }
+
+
+  public function getSubQueries() : array {
+    return $this->_sub_queries;
+  }
+
+
+  public function getJoins() : array {
+    return [];
+  }
+
+
+  public function select(?array $select = []) : self {
+    $this->getHelper()->addSelect($select);
+
+    return $this;
+  }
+
+
+  public function getHelper() : Storm_Query_Helper {
+    return $this->_helper;
+  }
+
+
+  public function limit(int $limit) : self {
+    $this->_clause_limit = Storm_Query_Clause::limit($limit);
+
+    return $this;
+  }
+
+
+  /**
+   * @param $range array | int | string
+   */
+  public function limit_page($range) : self {
+    $this->_clause_limit_page = Storm_Query_Clause::limitPage($range);
+
+    return $this;
+  }
+
+
+  /** interface Storm_Query_CriteriaInterface */
+
+  public function beOr() : self {
+    $this->_criteria->beOr();
+
+    return $this;
+  }
+
+
+  public function or(Storm_Query_CriteriaInterface $criteria) : self {
+    $this->_criteria->or($criteria->setAlias($this->getHelper()->getIdentifier()));
+
+    return $this;
+  }
+
+
+  public function and(Storm_Query_CriteriaInterface $criteria) : self {
+    $this->_criteria->and($criteria->setAlias($this->getHelper()->getIdentifier()));
+
+    return $this;
+  }
+
+
+  /**
+   * @param $value int | string
+   */
+  public function eq(string $key, $value) : self {
+    $this->_criteria->eq($key, $value);
+
+    return $this;
+  }
+
+
+  /**
+   * @param $value int | string
+   */
+  public function not_eq(string $key, $value) : self {
+    $this->_criteria->not_eq($key, $value);
+
+    return $this;
+  }
+
+
+  public function like(string $key, string $value) : self {
+    $this->_criteria->like($key, $value);
+
+    return $this;
+  }
+
+
+  public function not_like(string $key, string $value) : self {
+    $this->_criteria->not_like($key, $value);
+
+    return $this;
+  }
+
+
+  /**
+   * @param $value int | string
+   */
+  public function gt(string $key, $value) : self {
+    $this->_criteria->gt($key, $value);
+
+    return $this;
+  }
+
+
+  /**
+   * @param $value int | string
+   */
+  public function gt_eq(string $key, $value) : self {
+    $this->_criteria->gt_eq($key, $value);
+
+    return $this;
+  }
+
+
+  /**
+   * @param $value int | string
+   */
+  public function lt(string $key, $value) : self {
+    $this->_criteria->lt($key, $value);
+
+    return $this;
+  }
+
+
+  /**
+   * @param $value int | string
+   */
+  public function lt_eq(string $key, $value) : self {
+    $this->_criteria->lt_eq($key, $value);
+
+    return $this;
+  }
+
+
+  public function eq_length(string $key, int $length) : self {
+    $this->_criteria->eq_length($key, $length);
+
+    return $this;
+  }
+
+
+  public function not_eq_length(string $key, int $length) : self {
+    $this->_criteria->not_eq_length($key, $length);
+
+    return $this;
+  }
+
+
+  public function gt_length(string $key, int $length) : self {
+    $this->_criteria->gt_length($key, $length);
+
+    return $this;
+  }
+
+
+  public function gt_eq_length(string $key, int $length) : self {
+    $this->_criteria->gt_eq_length($key, $length);
+
+    return $this;
+  }
+
+
+  public function lt_length(string $key, int $length) : self {
+    $this->_criteria->lt_length($key, $length);
+
+    return $this;
+  }
+
+
+  public function lt_eq_length(string $key, int $length) : self {
+    $this->_criteria->lt_eq_length($key, $length);
+
+    return $this;
+  }
+
+
+  public function eq_left(string $key, int $width, string $value) : self {
+    $this->_criteria->eq_left($key, $width, $value);
+
+    return $this;
+  }
+
+
+  public function not_eq_left(string $key, int $width, string $value) : self {
+    $this->_criteria->not_eq_left($key, $width, $value);
+
+    return $this;
+  }
+
+
+  public function gt_left(string $key, int $width, string $value) : self {
+    $this->_criteria->gt_left($key, $width, $value);
+
+    return $this;
+  }
+
+
+  public function gt_eq_left(string $key, int $width, string $value) : self {
+    $this->_criteria->gt_eq_left($key, $width, $value);
+
+    return $this;
+  }
+
+
+  public function lt_left(string $key, int $width, string $value) : self {
+    $this->_criteria->lt_left($key, $width, $value);
+
+    return $this;
+  }
+
+
+  public function lt_eq_left(string $key, int $width, string $value) : self {
+    $this->_criteria->lt_eq_left($key, $width, $value);
+
+    return $this;
+  }
+
+
+  public function is_null(string $key) : self {
+    $this->_criteria->is_null($key);
+
+    return $this;
+  }
+
+
+  public function not_is_null(string $key) : self {
+    $this->_criteria->not_is_null($key);
+
+    return $this;
+  }
+
+
+  /**
+   * @param $array_or_subquery array | Storm_Query_In
+   */
+  public function in(string $key, $array_or_subquery) : self {
+    if ($array_or_subquery instanceof Storm_Query_In) {
+      $this->_sub_queries [] = $this->_updateIn($key, $array_or_subquery);
+
+      return $this;
+    }
+
+    $this->_criteria->in($key, $array_or_subquery);
+    return $this;
+  }
+
+
+  /**
+   * @param $array_or_subquery array | Storm_Query_In
+   */
+  public function not_in(string $key, $array_or_subquery) : self {
+    if ($array_or_subquery instanceof Storm_Query_In) {
+      $this->_sub_queries [] = $this->_updateIn($key, $array_or_subquery)->beNot();
+
+      return $this;
+    }
+
+    $this->_criteria->not_in($key, $array_or_subquery);
+    return $this;
+  }
+
+
+  protected function _updateIn(string $key, Storm_Query_In $sub_query) : Storm_Query_In {
+    return $sub_query->setParentHelper($this->getHelper())
+                     ->setCondition(Storm_Query_Condition::newWith($sub_query,
+                                                                   $key,
+                                                                   $sub_query->getHelper()
+                                                                   ->getFirstSelect()));
+  }
+
+
+  public function start(string $key, string $value) : self {
+    $this->_criteria->start($key, $value);
+
+    return $this;
+  }
+
+
+  public function end(string $key, string $value) : self {
+    $this->_criteria->end($key, $value);
+
+    return $this;
+  }
+
+
+  public function match(Storm_Query_MatchBoolean $match) : self {
+    $this->_criteria->match($match);
+
+    return $this;
+  }
+
+
+  public function exists(Storm_Query_Exists $exists) : self {
+    $this->_sub_queries [] = $exists->setParentHelper($this->getHelper());
+
+    return $this;
+  }
+
+
+  public function not_exists(Storm_Query_Exists $exists) : self {
+    $this->_sub_queries [] = $exists->beNot()->setParentHelper($this->getHelper());
+
+    return $this;
+  }
+
+
+  /** DB management */
+
+  public function assembleDb() : string {
+    return $this->getAssembleSelect()->assemble();
+  }
+
+
+  public function getAssembleSelect() : Zend_Db_Table_Select {
+    $select = $this->getHelper()->getLoader()->getTable()->select();
+    $this->_assembleFrom($select);
+    $this->_assembleWhere($select);
+    $this->_assembleLimit($select);
+    $this->_assembleLimitPage($select);
+
+    return $select;
+  }
+
+
+  protected function _assembleFrom(Zend_Db_Table_Select $select) : self {
+    $select->from($this->getHelper()->table(), $this->getHelper()->select());
+
+    return $this;
+  }
+
+
+  protected function _assembleWhere(Zend_Db_Table_Select $select) : self {
+    if ($where = $this->_sqlWhere())
+      $select->where($where);
+
+    return $this;
+  }
+
+
+  protected function _sqlWhere() : string {
+    $sql = new Storm_Query_Sql;
+    $this->_criteria->assemble($sql);
+
+    foreach ($this->getSubQueries() as $sub)
+      $sql->write($sub->sql());
+
+    return $sql->implode($this->_criteria->separator());
+  }
+
+
+  protected function _assembleLimit(Zend_Db_Table_Select $select) : self {
+    if ($this->_clause_limit)
+      $this->_clause_limit->assemble($select);
+
+    return $this;
+  }
+
+
+  protected function _assembleLimitPage(Zend_Db_Table_Select $select) : self {
+    if ($this->_clause_limit_page)
+      $this->_clause_limit_page->assemble($select);
+
+    return $this;
+  }
+
+
+  /** Volatile management */
+
+  public function assembleVolatile() : string {
+    return ($errors = implode(', ', $this->_errors))
+      ? $errors
+      : $this->assembleDb();
+  }
+
+
+  public function computeErrors() : array {
+    foreach ($this->getSubQueries() as $sub)
+      $this->_errors = [...$this->_errors, ...$sub->computeErrors()];
+
+    return $this->_errors;
+  }
+
+
+  public function instances(?array $instances = []) : array {
+    $instances [$this->getHelper()->getAlias()] = $this->_getFilteredInstances();
+
+    foreach ($this->getSubQueries() as $query)
+      $instances = $query->instances($instances);
+
+    return $instances;
+  }
+
+
+  public function getMatchingInstances() : array {
+    return $this->computeErrors()
+      ? []
+      : $this->_distinctInstances($this->_filterInstances());
+  }
+
+
+  protected function _distinctInstances(array $instances) : array {
+    if ( ! $this->_distinct)
+      return $instances;
+
+    $old_instances = [];
+    return array_filter(array_map(function($instance) use (&$old_instances) {
+      if ($this->_arrayContain($old_instances, $instance))
+        return null;
+
+      $old_instances [] = $instance;
+      return $instance;
+    },
+                                  $instances));
+  }
+
+
+  protected function _arrayContain(array $old_instances, array $instance) : bool {
+    $old_instance = array_shift($old_instances);
+    if (null === $old_instance)
+      return false;
+
+    $is_same = true;
+    foreach ($old_instance as $k => $v)
+      if (in_array($k, $this->getHelper()->select()) && $v !== $instance[$k])
+        $is_same = false;
+
+    return $is_same
+      ? true
+      : $this->_arrayContain($old_instances, $instance);
+  }
+
+
+  protected function _filterInstances() : array {
+    $query_instances = new Storm_Query_Instances($this);
+    $this->filterWith($query_instances);
+
+    return $query_instances->resultFor($this);
+  }
+
+
+  public function filterWith(Storm_Query_Instances $query_instances) : self {
+    foreach ($this->getSubQueries() as $sub)
+      $sub->filterWith($query_instances);
+
+    return $this;
+  }
+
+
+  protected function _getFilteredInstances() : array {
+    $instances = array_values(array_map(fn($model) => $model->getRawAttributes(),
+                                        $this->getHelper()
+                                        ->getLoader()
+                                        ->getPersistenceStrategy()
+                                        ->getInstances()));
+
+    return array_filter($instances,
+                        fn($model) => $this->getCriteria()->containsAllAttributes($model));
+  }
+
+
+  public function getLimitValue() : string {
+    return $this->_clause_limit
+      ? $this->_clause_limit->getValue()
+      : '';
+  }
+
+
+  public function getLimitPageValue() : array {
+    return $this->_clause_limit_page
+      ? $this->_clause_limit_page->getValue()
+      : [];
+  }
+}
diff --git a/src/Storm/Query/Clause.php b/src/Storm/Query/Clause.php
index 301d2711b0ae07ce010ec002eb209da3c74bb8a2..336f8ecc3ab8c863533eb82399ad92f8c1551f16 100644
--- a/src/Storm/Query/Clause.php
+++ b/src/Storm/Query/Clause.php
@@ -27,17 +27,19 @@ THE SOFTWARE.
 
 class Storm_Query_Clause {
 
-  protected string $_key;
+  protected Storm_Query_ClauseKey $_key;
   protected string $_operator;
-  protected bool $_negated;
+  protected string $_alias = '';
+  protected bool $_negated = false;
   protected $_value;
 
   const
     CLAUSE_WHERE = 'where',
-    CLAUSE_LIKE = 'like',
+    CLAUSE_LIKE = 'LIKE',
     CLAUSE_EQUAL = '=',
-    CLAUSE_IN = 'in',
-    CLAUSE_IS = 'is',
+    CLAUSE_NOT_EQUAL = '!=',
+    CLAUSE_IN = 'IN',
+    CLAUSE_IS = 'IS',
     CLAUSE_GREATER = '>',
     CLAUSE_GREATER_EQUAL = '>=',
     CLAUSE_LESSER = '<',
@@ -47,6 +49,8 @@ class Storm_Query_Clause {
     CLAUSE_LIMIT_PAGE = 'limitPage',
     CLAUSE_ORDER_BY = 'order by',
     CLAUSE_GROUP_BY = 'group by',
+    CLAUSE_NOT = ' NOT ',
+    CLAUSE_EXISTS = 'EXISTS',
     PERCENT = '%';
 
   const CLAUSE_IMPLEMENTATIONS =
@@ -62,7 +66,6 @@ class Storm_Query_Clause {
      self::CLAUSE_MATCH => Storm_Query_Clause_Match::class,
      self::CLAUSE_LIMIT => Storm_Query_ClauseLimit::class,
      self::CLAUSE_LIMIT_PAGE => Storm_Query_ClauseLimitPage::class,
-     self::CLAUSE_ORDER_BY => Storm_Query_Clause_OrderBy::class,
      self::CLAUSE_GROUP_BY => Storm_Query_ClauseGroupBy::class,
      self::CLAUSE_WHERE => Storm_Query_ClauseWhere::class
     ];
@@ -70,8 +73,9 @@ class Storm_Query_Clause {
   /**
    * @param $value null | string | Storm_Query_Clause
    */
-  public function __construct(string $key, string $operator, $value) {
-    $this->_negated = false;
+  public function __construct(Storm_Query_ClauseKey $key,
+                              string $operator,
+                              $value) {
     $this->_key = $key;
     $this->_operator = $operator;
     $this->_value = $value;
@@ -85,10 +89,11 @@ class Storm_Query_Clause {
   }
 
 
-  public static function newFor(string $key,
+  public static function newFor(Storm_Query_ClauseKey $key,
                                 string $operator,
                                 $value) : self {
     $class = static::CLAUSE_IMPLEMENTATIONS[$operator];
+
     return new $class($key, $operator, $value);
   }
 
@@ -106,68 +111,94 @@ class Storm_Query_Clause {
     if ($is_like)
       $operator = static::CLAUSE_LIKE;
 
+    $matches = null;
+    if (!$is_like)
+      preg_match('/^left\(([^,]+),\s+(\d+)\)$/i', $key, $matches);
+    if ($matches)
+      return (static::newFor((new Storm_Query_ClauseKey(trim($matches[1])))
+                             ->setLeft((int)$matches[2]),
+                             $operator,
+                             $value))
+        ->setNegated($negated);
+
     if (null === $value)
       $operator = static::CLAUSE_IS;
 
     if (is_array($value))
       $operator = static::CLAUSE_IN;
 
-    return (static::newFor($key, $operator, $value))->setNegated($negated);
+    return (static::newFor(new Storm_Query_ClauseKey($key), $operator, $value))
+      ->setNegated($negated);
   }
 
 
   public static function isNull(string $key) : self {
-    return static::newFor($key, static::CLAUSE_IS, null);
+    return static::newFor(static::_initKey($key), static::CLAUSE_IS, null);
   }
 
 
   public static function in(string $key, array $array) : self {
-    return static::newFor($key, static::CLAUSE_IN, $array);
+    return static::newFor(static::_initKey($key), static::CLAUSE_IN, $array);
+  }
+
+
+  public static function equal($key, $value) : self {
+    return static::newFor(static::_initKey($key), static::CLAUSE_EQUAL, $value);
   }
 
 
-  public static function equal(string $key, $value) : self {
-    return static::newFor($key, static::CLAUSE_EQUAL, $value);
+  public static function left(string $key, int $left, string $value) : self {
+    return static::newFor(static::_initKey($key)->setLeft($left), static::CLAUSE_EQUAL, $value);
   }
 
 
   public static function like(string $key, string $value) : self {
-    return static::newFor($key, static::CLAUSE_LIKE, $value);
+    return static::newFor(static::_initKey($key), static::CLAUSE_LIKE, $value);
   }
 
 
   public static function start(string $key, string $value) : self {
-    return static::newFor($key, static::CLAUSE_LIKE, $value . static::PERCENT);
+    return static::newFor(static::_initKey($key),
+                          static::CLAUSE_LIKE,
+                          $value . static::PERCENT);
   }
 
 
   public static function end(string $key, string $value) : self {
-    return static::newFor($key, static::CLAUSE_LIKE, static::PERCENT . $value);
+    return static::newFor(static::_initKey($key),
+                          static::CLAUSE_LIKE,
+                          static::PERCENT . $value);
   }
 
 
-  public static function greater(string $key, $value) : self {
-    return static::newFor($key, static::CLAUSE_GREATER, $value);
+  public static function greater($key, $value) : self {
+    return static::newFor(static::_initKey($key), static::CLAUSE_GREATER, $value);
   }
 
 
-  public static function greaterEqual(string $key, $value) : self {
-    return static::newFor($key, static::CLAUSE_GREATER_EQUAL, $value);
+  public static function greaterEqual($key, $value) : self {
+    return static::newFor(static::_initKey($key),
+                          static::CLAUSE_GREATER_EQUAL,
+                          $value);
   }
 
 
-  public static function lesser(string $key, $value) : self {
-    return static::newFor($key, static::CLAUSE_LESSER, $value);
+  public static function lesser($key, $value) : self {
+    return static::newFor(static::_initKey($key), static::CLAUSE_LESSER, $value);
   }
 
 
-  public static function lesserEqual(string $key, $value) : self {
-    return static::newFor($key, static::CLAUSE_LESSER_EQUAL, $value);
+  public static function lesserEqual($key, $value) : self {
+    return static::newFor(static::_initKey($key),
+                          static::CLAUSE_LESSER_EQUAL,
+                          $value);
   }
 
 
   public static function match(Storm_Query_MatchRating $match) : self {
-    return static::newFor($match->getKey(), static::CLAUSE_MATCH, $match);
+    return static::newFor(static::_initKey($match->getKey()),
+                          static::CLAUSE_MATCH,
+                          $match);
   }
 
 
@@ -175,7 +206,9 @@ class Storm_Query_Clause {
    * @param $limit int | string
    */
   public static function limit($limit) : self {
-    return static::newFor(static::CLAUSE_LIMIT, static::CLAUSE_LIMIT, $limit);
+    return static::newFor(static::_initKey(static::CLAUSE_LIMIT),
+                          static::CLAUSE_LIMIT,
+                          $limit);
   }
 
 
@@ -183,25 +216,41 @@ class Storm_Query_Clause {
    * @param $range array | int | string
    */
   public static function limitPage($range) : self {
-    return static::newFor(static::CLAUSE_LIMIT_PAGE,
+    return static::newFor(static::_initKey(static::CLAUSE_LIMIT_PAGE),
                           static::CLAUSE_LIMIT_PAGE,
                           $range);
   }
 
 
   public static function group(string $value) : self {
-    return static::newFor(static::CLAUSE_GROUP_BY, static::CLAUSE_GROUP_BY, $value);
+    return static::newFor(static::_initKey(static::CLAUSE_GROUP_BY),
+                          static::CLAUSE_GROUP_BY,
+                          $value);
   }
 
 
-  public static function order(string $key,
-                               ?Storm_Query_Clause $clause = null) : self {
-    return static::newFor($key, static::CLAUSE_ORDER_BY, $clause);
+  public static function order(string $key, $value = null) : self {
+    return Storm_Query_Clause_OrderBy::newFor(static::_initKey($key), static::CLAUSE_ORDER_BY, $value);
+  }
+
+
+  protected static function _initKey($key) : Storm_Query_ClauseKey {
+    return ($key instanceof Storm_Query_ClauseKey)
+      ? $key
+      : new Storm_Query_ClauseKey($key);
   }
 
 
   public function setNegated(bool $negated) : self {
     $this->_negated = $negated;
+
+    return $this;
+  }
+
+
+  public function setAlias(string $alias) : self {
+    $this->_alias = $alias;
+
     return $this;
   }
 
@@ -211,41 +260,43 @@ class Storm_Query_Clause {
   }
 
 
-  public function assemble($select) : self {
+  public function assemble(Zend_Db_Table_Select $select) : self {
     return $this;
   }
 
 
-  public function getFormatDb() : string {
+  public function getFormatDb(?bool $with_quote = true) : string {
     if (static::CLAUSE_WHERE === $this->_operator)
       return '(' . $this->getValue() . ')';
 
-    return Zend_Db_Table_Abstract::getDefaultAdapter()
+    return $with_quote
+      ? Zend_Db_Table_Abstract::getDefaultAdapter()
       ->quoteInto($this->_clauseFormatDb(),
-                  $this->getValue(), null, null);
+                  $this->getValue(), null, null)
+      : str_replace('?', $this->getValue(), $this->_clauseFormatDb());
   }
 
 
   public function containAttibuteInVolatile(array $model) : bool {
-    if (!$this->_existKeyInModel($model))
+    if (!$this->_key->existInModel($model))
       return $this->_negated;
 
-    return ($this->_negated !== ($model[$this->_key] == $this->getValue()));
+    return $this->contains($this->_key->keyFrom($model), $this->getValue());
   }
 
 
-  protected function _clauseFormatDb() : string {
-    return $this->_key . $this->_getOperator() . '?';
+  public function contains($value_1, $value_2) : bool {
+    return ($this->_negated !== ($value_1 == $value_2));
   }
 
 
-  protected function _getOperator() : string {
-    return ($this->_negated ? 'not ' : '') . $this->_operator;
+  protected function _clauseFormatDb() : string {
+    return $this->_key->key($this->_alias) . $this->_getOperator() . '?';
   }
 
 
-  protected function _existKeyInModel(array $model) : bool {
-    return array_key_exists($this->_key, $model);
+  protected function _getOperator() : string {
+    return ($this->_negated ? static::CLAUSE_NOT : ' ') . $this->_operator . ' ';
   }
 }
 
@@ -261,18 +312,9 @@ class Storm_Query_ClauseLike extends Storm_Query_Clause {
   }
 
 
-  protected function _clauseFormatDb() : string {
-    return $this->_key . ' ' . $this->_getOperator() . ' ?';
-  }
-
-
-  public function containAttibuteInVolatile(array $model) : bool {
-    if (!$this->_existKeyInModel($model))
-      return $this->_negated;
-
-    $matches =
-      preg_match(('/^' . str_replace('%', '.*', preg_quote($this->getValue(), '/')) . '$/i'),
-                 $model[$this->_key]);
+  public function contains($value_1, $value_2) : bool {
+    $matches = preg_match(('/^' . str_replace('%', '.*', preg_quote($value_2, '/')) . '$/i'),
+                          $value_1);
 
     return $this->_negated ? !$matches : $matches;
   }
@@ -284,7 +326,7 @@ class Storm_Query_ClauseLike extends Storm_Query_Clause {
 class Storm_Query_ClauseEqual extends Storm_Query_Clause {
 
   protected function _getOperator() : string {
-    return ($this->_negated ? '!' : '') . $this->_operator;
+    return ' ' . ($this->_negated ? static::CLAUSE_NOT_EQUAL : $this->_operator) . ' ';
   }
 }
 
@@ -302,21 +344,38 @@ class Storm_Query_ClauseIn extends Storm_Query_Clause {
 
 
   public function containAttibuteInVolatile(array $model) : bool {
-    if (!$this->_existKeyInModel($model))
+    if (!$this->_key->existInModel($model))
       return $this->_negated;
 
     if (0 === count($this->getValue()))
       throw new Storm_Model_Exception(sprintf('array given for %s is empty',
-                                              $this->_key));
+                                              $this->_key->getKey()));
+
+    return $this->contains($this->_key->keyFrom($model), $this->getValue());
+  }
+
+
+  public function contains($value_1, $value_2) : bool {
+    if ( ! is_array($value_2))
+      $value_2 = [$value_2];
 
-    return in_array($model[$this->_key], $this->getValue())
+    return in_array($value_1, $value_2)
       ? !$this->_negated
       : $this->_negated;
   }
 
 
+  public function getFormatDb(?bool $with_quote = true) : string {
+    return $with_quote
+      ? Zend_Db_Table_Abstract::getDefaultAdapter()
+      ->quoteInto($this->_clauseFormatDb(),
+                  $this->getValue(), null, null)
+      : str_replace('?', implode(', ', $this->getValue()), $this->_clauseFormatDb());
+  }
+
+
   protected function _clauseFormatDb() : string {
-    return $this->_key . ' ' . $this->_getOperator() . ' (?)';
+    return $this->_key->key($this->_alias) . $this->_getOperator() . '(?)';
   }
 }
 
@@ -332,13 +391,13 @@ class Storm_Query_ClauseIsNull extends Storm_Query_Clause {
   }
 
 
-  public function getFormatDb() : string {
-    return $this->_key . ' ' . $this->_getOperator() . ' null';
+  public function getFormatDb(?bool $with_quote = true) : string {
+    return $this->_key->key($this->_alias) . $this->_getOperator() . 'null';
   }
 
 
   protected function _getOperator() : string {
-    return $this->_operator . ($this->_negated ? ' not' : '');
+    return ' ' . $this->_operator . ($this->_negated ? static::CLAUSE_NOT : ' ');
   }
 }
 
@@ -347,11 +406,8 @@ class Storm_Query_ClauseIsNull extends Storm_Query_Clause {
 
 class Storm_Query_ClauseGreater extends Storm_Query_Clause {
 
-  public function containAttibuteInVolatile(array $model) : bool {
-    if (!$this->_existKeyInModel($model))
-      return false;
-
-    return $model[$this->_key] > $this->getValue();
+  public function contains($value_1, $value_2) : bool {
+    return $value_1 > $value_2;
   }
 }
 
@@ -360,11 +416,8 @@ class Storm_Query_ClauseGreater extends Storm_Query_Clause {
 
 class Storm_Query_ClauseGreaterEqual extends Storm_Query_Clause {
 
-  public function containAttibuteInVolatile(array $model) : bool {
-    if (!$this->_existKeyInModel($model))
-      return false;
-
-    return $model[$this->_key] >= $this->getValue();
+  public function contains($value_1, $value_2) : bool {
+    return $value_1 >= $value_2;
   }
 }
 
@@ -373,11 +426,8 @@ class Storm_Query_ClauseGreaterEqual extends Storm_Query_Clause {
 
 class Storm_Query_ClauseLesser extends Storm_Query_Clause {
 
-  public function containAttibuteInVolatile(array $model) : bool {
-    if (!$this->_existKeyInModel($model))
-      return false;
-
-    return $model[$this->_key] < $this->getValue();
+  public function contains($value_1, $value_2) : bool {
+    return $value_1 < $value_2;
   }
 }
 
@@ -386,11 +436,8 @@ class Storm_Query_ClauseLesser extends Storm_Query_Clause {
 
 class Storm_Query_ClauseLesserEqual extends Storm_Query_Clause {
 
-  public function containAttibuteInVolatile(array $model) : bool {
-    if (!$this->_existKeyInModel($model))
-      return false;
-
-    return $model[$this->_key] <= $this->getValue();
+  public function contains($value_1, $value_2) : bool {
+    return $value_1 <= $value_2;
   }
 }
 
@@ -414,7 +461,7 @@ class Storm_Query_ClauseLimit extends Storm_Query_Clause {
   }
 
 
-  public function getFormatDb() : string {
+  public function getFormatDb(?bool $with_quote = true) : string {
     return '';
   }
 
@@ -451,7 +498,7 @@ class Storm_Query_ClauseLimitPage extends Storm_Query_Clause {
   }
 
 
-  public function getFormatDb() : string {
+  public function getFormatDb(?bool $with_quote = true) : string {
     return '';
   }
 
@@ -474,12 +521,17 @@ class Storm_Query_ClauseGroupBy extends Storm_Query_Clause {
 
 
   public function assemble($select) : self {
-    $select->group($this->getValue());
+    $alias = '';
+    if ($this->_alias)
+      $alias = $this->_alias . '.';
+
+    $select->group($alias . $this->getValue());
+
     return $this;
   }
 
 
-  public function getFormatDb() : string {
+  public function getFormatDb(?bool $with_quote = true) : string {
     return '';
   }
 
@@ -505,3 +557,67 @@ class Storm_Query_ClauseWhere extends Storm_Query_Clause {
     return true;
   }
 }
+
+
+
+
+class Storm_Query_ClauseKey {
+
+  protected string $_key;
+  protected ?int $_left = null;
+  protected bool $_length = false;
+
+  public function __construct(string $key) {
+    $this->_key = $key;
+  }
+
+
+  public function getKey() : string {
+    return $this->_key;
+  }
+
+
+  public function beLength() : self {
+    $this->_length = true;
+
+    return $this;
+  }
+
+
+  public function setLeft(int $left) : self {
+    $this->_left = $left;
+
+    return $this;
+  }
+
+
+  public function key(string $alias = '') : string {
+    $key = Zend_Db_Table_Abstract::getDefaultAdapter()->quoteIdentifier($this->_key, true);
+
+    if ($alias)
+      $key = $alias . '.' . $key;
+
+    if ($this->_length)
+      $key = 'LENGTH(' . $key . ')';
+
+    if (null !== $this->_left)
+      $key = sprintf('LEFT(%s, %d)', $key, $this->_left);
+
+    return $key;
+  }
+
+
+  public function keyFrom(array $model) {
+    if (null !== $this->_left)
+      return substr($model[$this->_key] ?? '', 0, $this->_left);
+
+    return $this->_length
+      ? strlen($model[$this->_key] ?? '')
+      : $model[$this->_key] ?? null;
+  }
+
+
+  public function existInModel(array $model) : bool {
+    return array_key_exists($this->_key, $model);
+  }
+}
diff --git a/src/Storm/Query/Clause/Match.php b/src/Storm/Query/Clause/Match.php
index e7f48b9810fc03c3cd622844b71b207333d4bceb..240ed6845ca7d3156d5c1c569073aa193c9bd3f9 100644
--- a/src/Storm/Query/Clause/Match.php
+++ b/src/Storm/Query/Clause/Match.php
@@ -27,7 +27,7 @@ THE SOFTWARE.
 
 class Storm_Query_Clause_Match extends Storm_Query_Clause {
 
-  public function getFormatDb() : string {
+  public function getFormatDb(?bool $with_quote = true) : string {
     if (!$value = $this->getValue())
       return '';
 
@@ -40,7 +40,7 @@ class Storm_Query_Clause_Match extends Storm_Query_Clause {
                        ->quoteInto('?', $content),
                        ($value->isBooleanMode() ? ' IN BOOLEAN MODE' : ''));
 
-    return $this->_getOperator() . ' ' . $against;
+    return $this->_clauseFormatDb() . ' ' . $against;
   }
 
 
@@ -89,7 +89,7 @@ class Storm_Query_Clause_Match extends Storm_Query_Clause {
 
 
   protected function _getOperator() : string {
-    return $this->_operator . '(' . $this->_key . ')';
+    return $this->_operator . '(' . $this->_key->getKey() . ')';
   }
 
 
@@ -100,7 +100,7 @@ class Storm_Query_Clause_Match extends Storm_Query_Clause {
 
   protected function _getContents(array $model) : string {
     $contents = '';
-    foreach (explode(',', $this->_key) as $key)
+    foreach (explode(',', $this->_key->getKey()) as $key)
       if (array_key_exists($key, $model))
         $contents = implode(' ', array_filter([$contents, $model[$key]]));
 
diff --git a/src/Storm/Query/Clause/OrderBy.php b/src/Storm/Query/Clause/OrderBy.php
index 72deff1ba0c1f24b4f10d04cdde4aa47592c29d2..fbe6195ab3c5dfe3674f8d2bbbed08a7c30d0953 100644
--- a/src/Storm/Query/Clause/OrderBy.php
+++ b/src/Storm/Query/Clause/OrderBy.php
@@ -27,20 +27,32 @@ THE SOFTWARE.
 
 class Storm_Query_Clause_OrderBy extends Storm_Query_Clause {
 
-  const ORDER_DESC = ' desc';
+  const ORDER_DESC = ' DESC';
+  const ORDER_ASC = ' ASC';
 
-  protected ?string $_order_mode = null;
+  protected string $_order_mode = self::ORDER_ASC;
+
+  public static function newFor(Storm_Query_ClauseKey $key,
+                                string $operator,
+                                $value) : self {
+    if ( ! $value)
+      return new Storm_Query_Clause_OrderBy($key, $operator, null);
+
+    if ($value instanceof Storm_Query_Clause_Match)
+      return new Storm_Query_Clause_OrderByForMatch($key, $operator, $value);
+
+    return new Storm_Query_Clause_OrderByWithValue($key, $operator, $value);
+  }
+
+
+  public function assemble(Zend_Db_Table_Select $select) : self {
+    $select->order(new Zend_Db_Expr($this->_key->key($this->_alias) . $this->_order_mode));
 
-  public function assemble($select) : self {
-    $select->order((($clause = $this->getValue())
-                    ? $clause->getFormatDb($select->getTable())
-                    : $this->_key)
-                   . $this->_order_mode);
     return $this;
   }
 
 
-  public function getFormatDb() : string {
+  public function getFormatDb(?bool $with_quote = true) : string {
     return '';
   }
 
@@ -50,8 +62,9 @@ class Storm_Query_Clause_OrderBy extends Storm_Query_Clause {
   }
 
 
-  public function setOrder(bool $mode) : self {
-    $this->_order_mode = $mode ? static::ORDER_DESC : '';
+  public function beDesc() : self {
+    $this->_order_mode = static::ORDER_DESC;
+
     return $this;
   }
 
@@ -68,11 +81,8 @@ class Storm_Query_Clause_OrderBy extends Storm_Query_Clause {
 
 
   protected function _compareValues(array $a, array $b) : int {
-    if ($match_clause = $this->getValue())
-      return $match_clause->compare($a, $b);
-
-    $first = $this->_existKeyInModel($a) ? $a[$this->_key] : '';
-    $second = $this->_existKeyInModel($b) ? $b[$this->_key] : '';
+    $first = $this->_key->existInModel($a) ? $a[$this->_key->getKey()] : '';
+    $second = $this->_key->existInModel($b) ? $b[$this->_key->getKey()] : '';
 
     return ($first <=> $second);
   }
@@ -82,3 +92,50 @@ class Storm_Query_Clause_OrderBy extends Storm_Query_Clause {
     return static::ORDER_DESC === $this->_order_mode ? -1 : 1;
   }
 }
+
+
+
+
+class Storm_Query_Clause_OrderByForMatch extends Storm_Query_Clause_OrderBy {
+
+  public function assemble(Zend_Db_Table_Select $select) : self {
+    $select->order($this->getValue()->getFormatDb() . $this->_order_mode);
+
+    return $this;
+  }
+
+
+  protected function _compareValues(array $a, array $b) : int {
+    return $this->getValue()->compare($a, $b);
+  }
+}
+
+
+
+
+class Storm_Query_Clause_OrderByWithValue extends Storm_Query_Clause_OrderBy {
+
+  public function assemble(Zend_Db_Table_Select $select) : self {
+    $select->order(new Zend_Db_Expr($this->_key->key($this->_alias)
+                                    . ' ' . Storm_Query_Clause::CLAUSE_EQUAL . ' '
+                                    . Zend_Db_Table_Abstract::getDefaultAdapter()
+                                    ->quoteInto('?', $this->getValue(), null, null)
+                                    . $this->_order_mode));
+
+    return $this;
+  }
+
+
+  protected function _compareValues(array $a, array $b) : int {
+    $value = $this->getValue();
+
+    $first = $this->_key->existInModel($a) && $value === $a[$this->_key->getKey()]
+      ? 1
+      : 0;
+    $second = $this->_key->existInModel($b) && $value === $b[$this->_key->getKey()]
+      ? 1
+      : 0;
+
+    return ($first <=> $second);
+  }
+}
diff --git a/src/Storm/Query/Condition.php b/src/Storm/Query/Condition.php
new file mode 100644
index 0000000000000000000000000000000000000000..ea334f82b934b60c849ca20093998a7ab650d5fa
--- /dev/null
+++ b/src/Storm/Query/Condition.php
@@ -0,0 +1,348 @@
+<?php
+/*
+  STORM is under the MIT License (MIT)
+
+  Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+  THE SOFTWARE.
+
+*/
+
+
+class Storm_Query_Condition {
+
+  protected string $_parent_key;
+  protected string $_key;
+  protected string $_operator = Storm_Query_Clause::CLAUSE_EQUAL;
+  protected bool $_negated = false;
+  protected ?Storm_Query_Clause $_clause = null;
+  protected Storm_Query_Abstract $_query;
+  protected ?Storm_Query_Condition $_child_condition = null;
+
+  protected function __construct(Storm_Query_Abstract $query, string $parent_key, string $key) {
+    $this->_parent_key = $parent_key;
+    $this->_key = $key;
+    $this->_query = $query;
+  }
+
+
+  public static function newWith(Storm_Query_Abstract $query,
+                                 string $parent_key,
+                                 string $key) : self {
+    if ($query instanceof Storm_Join_Inner)
+      return new Storm_Query_ConditionInner($query, $parent_key, $key);
+    if ($query instanceof Storm_Join_Left)
+      return new Storm_Query_ConditionLeft($query, $parent_key, $key);
+    if ($query instanceof Storm_Join_Right)
+      return new Storm_Query_ConditionRight($query, $parent_key, $key);
+
+    if ($query instanceof Storm_Query_In)
+      return new Storm_Query_ConditionSubQueries($query, $parent_key, $key);
+
+    if ($query instanceof Storm_Query_Exists)
+      return new Storm_Query_ConditionSubQueries($query, $parent_key, $key);
+
+    return new static($query, $parent_key, $key);
+  }
+
+
+  public function getParentKey() : string {
+    return $this->_parent_key;
+  }
+
+
+  public function getQuery() : Storm_Query_Abstract {
+    return $this->_query;
+  }
+
+
+  public function add(Storm_Query_Condition $child) : self {
+    if ($this->_child_condition) {
+      $this->_child_condition->add($child);
+
+      return $this;
+    }
+
+    $this->_child_condition = $child;
+
+    return $this;
+  }
+
+
+  public function beNotEqual() : self {
+    $this->_negated = true;
+
+    return $this;
+  }
+
+
+  public function beGreater() : self {
+    $this->_operator = Storm_Query_Clause::CLAUSE_GREATER;
+
+    return $this;
+  }
+
+
+  public function beGreaterEqual() : self {
+    $this->_operator = Storm_Query_Clause::CLAUSE_GREATER_EQUAL;
+
+    return $this;
+  }
+
+
+  public function beLesser() : self {
+    $this->_operator = Storm_Query_Clause::CLAUSE_LESSER;
+
+    return $this;
+  }
+
+
+  public function beLesserEqual() : self {
+    $this->_operator = Storm_Query_Clause::CLAUSE_LESSER_EQUAL;
+
+    return $this;
+  }
+
+
+  public function beIn() : self {
+    $this->_operator = Storm_Query_Clause::CLAUSE_IN;
+
+    return $this;
+  }
+
+
+  public function beNotIn() : self {
+    $this->_operator = Storm_Query_Clause::CLAUSE_IN;
+    $this->_negated = true;
+
+    return $this;
+  }
+
+
+  public function sql() : array {
+    if (!$this->_query->getParentHelper())
+      return '';
+
+    if ( ! $this->_child_condition)
+      return [$this->_clause()->getFormatDb(false)];
+
+    return [$this->_clause()->getFormatDb(false),
+            ...$this->_child_condition->sql()];
+  }
+
+
+  protected function _clause() : Storm_Query_Clause {
+    if ($this->_clause)
+      return $this->_clause;
+
+    $value = ($this->_query->getHelper()->getIdentifier()
+              . '.' . Zend_Db_Table_Abstract::getDefaultAdapter()
+              ->quoteIdentifier($this->_key, true));
+
+    return Storm_Query_Clause::newFor(new Storm_Query_ClauseKey($this->_parent_key),
+                                      $this->_operator,
+                                      $value)
+      ->setNegated($this->_negated)
+      ->setAlias($this->_query->getParentHelper()->getIdentifier());
+  }
+
+
+  public function filterWith(Storm_Query_Instances $query_instances) : self {
+    if (!$this->_query->getParentHelper())
+      return $this;
+
+    return $this->_filter($query_instances);
+  }
+
+
+  protected function _filter(Storm_Query_Instances $query_instances) : self {
+    return $this;
+  }
+
+
+  public function conditionSelect(Storm_Query_Instance $parent_instance,
+                                  Storm_Query_Instance $instance) : bool {
+    $parent_value = $parent_instance->value($this->_query->getParentHelper()->getAlias(),
+                                            $this->_parent_key);
+    $value = $instance->value($this->_query->getHelper()->getAlias(), $this->_key);
+
+    $contains = $this->_clause()->contains($parent_value, $value);
+    if ( ! $this->_child_condition)
+      return $contains;
+
+    return $contains && $this->_child_condition->conditionSelect($parent_instance, $instance);
+  }
+
+
+  protected function _updateQueryInstances(Storm_Query_Instances $query_instances,
+                                           Storm_Collection $filter) : self {
+    $query_instances->addAll([$this->_query->getParentHelper()->getAlias(),
+                              $this->_query->getHelper()->getAlias()],
+                             $filter);
+
+    return $this;
+  }
+}
+
+
+
+
+class Storm_Query_ConditionInner extends Storm_Query_Condition {
+
+  protected function _filter(Storm_Query_Instances $query_instances) : self {
+    $alias_instances = $query_instances->instancesFor($this->_query->getHelper()->getAlias());
+    $condition_filter = new Storm_Query_ConditionFilter($this, $alias_instances);
+
+    $query_instances
+      ->instancesFor($this->_query->getParentHelper()->getAlias())
+      ->eachDo(fn($parent) => $condition_filter->updateCrossedWith($parent));
+
+    return $this->_updateQueryInstances($query_instances, $condition_filter->getFilter());
+  }
+}
+
+
+
+
+class Storm_Query_ConditionLeft extends Storm_Query_Condition {
+
+  protected function _filter(Storm_Query_Instances $query_instances) : self {
+    $alias_instances = $query_instances->instancesFor($this->_query->getHelper()->getAlias());
+    $condition_filter = new Storm_Query_ConditionFilter($this, $alias_instances);
+
+    $query_instances
+      ->instancesFor($this->_query->getParentHelper()->getAlias())
+      ->eachDo(fn($parent) => $condition_filter->updateOuterLeftWith($parent));
+
+    return $this->_updateQueryInstances($query_instances, $condition_filter->getFilter());
+  }
+}
+
+
+
+
+class Storm_Query_ConditionRight extends Storm_Query_Condition {
+
+  protected function _filter(Storm_Query_Instances $query_instances) : self {
+    $parent_instances = $query_instances
+      ->instancesFor($this->_query->getParentHelper()->getAlias());
+    $condition_filter = new Storm_Query_ConditionFilter($this, $parent_instances);
+
+    $query_instances
+      ->instancesFor($this->_query->getHelper()->getAlias())
+      ->eachDo(fn($child) => $condition_filter->updateOuterRightWith($child));
+
+    return $this->_updateQueryInstances($query_instances, $condition_filter->getFilter());
+  }
+}
+
+
+
+
+class Storm_Query_ConditionSubQueries extends Storm_Query_ConditionInner {
+
+  protected function _filter(Storm_Query_Instances $query_instances) : self {
+    $alias_instances = $query_instances->instancesFor($this->_query->getHelper()->getAlias());
+    $condition_filter = new Storm_Query_ConditionFilter($this, $alias_instances);
+
+    $query_instances
+      ->instancesFor($this->_query->getParentHelper()->getAlias())
+      ->eachDo(fn($parent) => $condition_filter->updateAnyWith($parent));
+
+    return $this->_updateQueryInstances($query_instances, $condition_filter->getFilter());
+  }
+}
+
+
+
+
+class Storm_Query_ConditionFilter {
+
+  protected Storm_Query_Condition $_condition;
+  protected Storm_Collection $_child_instances;
+  protected Storm_Collection $_filter;
+
+  public function __construct(Storm_Query_Condition $condition,
+                              Storm_Collection $child_instances) {
+    $this->_condition = $condition;
+    $this->_child_instances = $child_instances;
+    $this->_filter = new Storm_Collection;
+  }
+
+
+  public function getFilter() : Storm_Collection {
+    return $this->_filter;
+  }
+
+
+  public function updateCrossedWith(Storm_Query_Instance $parent) : self {
+    $this->_child_instances
+      ->select(fn($instance) => $this->_condition->conditionSelect($parent, $instance))
+      ->eachDo(fn($instance) => $this->_filter->add((new Storm_Query_Instance)
+                                                    ->addFrom($parent)
+                                                    ->addFrom($instance)));
+
+    return $this;
+  }
+
+
+  public function updateOuterLeftWith(Storm_Query_Instance $parent) : self {
+    $selected = $this->_child_instances
+      ->select(fn($instance) => $this->_condition->conditionSelect($parent, $instance));
+
+    return $this->_withSelectedDo($selected, $parent);
+  }
+
+
+  public function updateOuterRightWith(Storm_Query_Instance $parent) : self {
+    $selected = $this->_child_instances
+      ->select(fn($instance) => $this->_condition->conditionSelect($instance, $parent));
+
+    return $this->_withSelectedDo($selected, $parent);
+  }
+
+
+  protected function _withSelectedDo(Storm_Collection $selected,
+                                     Storm_Query_Instance $parent) : self {
+    if ($selected->isEmpty()) {
+      $this->_filter->add((new Storm_Query_Instance)->addFrom($parent));
+
+      return $this;
+    }
+
+    $selected
+      ->eachDo(fn($instance) => $this->_filter->add((new Storm_Query_Instance)
+                                                    ->addFrom($parent)
+                                                    ->addFrom($instance)));
+
+    return $this;
+  }
+
+
+  public function updateAnyWith(Storm_Query_Instance $parent) : self {
+    $exist = $this->_child_instances
+      ->detect(fn($instance) => $this->_condition->conditionSelect($parent, $instance));
+
+    if (($exist && ! $this->_condition->getQuery()->isNot())
+        || ( ! $exist && $this->_condition->getQuery()->isNot()))
+      $this->_filter->add((new Storm_Query_Instance)->addFrom($parent));
+
+    return $this;
+  }
+}
diff --git a/src/Storm/Query/Criteria.php b/src/Storm/Query/Criteria.php
index 48ba096139fb96df0ec4e623fe38dd899358b588..e1f4d9a8af4bd37deecbe5b997188b2f957ca144 100644
--- a/src/Storm/Query/Criteria.php
+++ b/src/Storm/Query/Criteria.php
@@ -28,12 +28,12 @@
 class Storm_Query_Criteria implements Storm_Query_CriteriaInterface {
 
   const
-    SEPARATOR_AND = 'and',
-    SEPARATOR_OR = 'or';
+    SEPARATOR_AND = 'AND',
+    SEPARATOR_OR = 'OR';
 
   protected array $_clauses;
   protected string $_separator;
-
+  protected string $_alias = '';
 
   public function __construct() {
     $this->_clauses = [];
@@ -41,6 +41,12 @@ class Storm_Query_Criteria implements Storm_Query_CriteriaInterface {
   }
 
 
+  public function setAlias(string $alias) : self {
+    $this->_alias = $alias;
+    return $this;
+  }
+
+
   public function beOr() : self {
     $this->_separator = static::SEPARATOR_OR;
     return $this;
@@ -71,7 +77,8 @@ class Storm_Query_Criteria implements Storm_Query_CriteriaInterface {
       return '(' . $sql->implode($clause->separator()) . ')';
     }
 
-    return $clause->getFormatDb();
+    return $clause->setAlias($this->_alias)
+                  ->getFormatDb();
   }
 
 
@@ -113,6 +120,7 @@ class Storm_Query_Criteria implements Storm_Query_CriteriaInterface {
    */
   public function eq(string $key, $value) : self {
     $this->_clauses [] = Storm_Query_Clause::equal($key, $value);
+
     return $this;
   }
 
@@ -123,12 +131,14 @@ class Storm_Query_Criteria implements Storm_Query_CriteriaInterface {
   public function not_eq(string $key, $value) : self {
     $this->_clauses [] = Storm_Query_Clause::equal($key, $value)
       ->setNegated(true);
+
     return $this;
   }
 
 
   public function like(string $key, string $value) : self {
     $this->_clauses [] = Storm_Query_Clause::like($key, $value);
+
     return $this;
   }
 
@@ -136,6 +146,7 @@ class Storm_Query_Criteria implements Storm_Query_CriteriaInterface {
   public function not_like(string $key, string $value) : self {
     $this->_clauses [] = Storm_Query_Clause::like($key, $value)
       ->setNegated(true);
+
     return $this;
   }
 
@@ -145,6 +156,7 @@ class Storm_Query_Criteria implements Storm_Query_CriteriaInterface {
    */
   public function gt(string $key, $value) : self {
     $this->_clauses [] = Storm_Query_Clause::greater($key, $value);
+
     return $this;
   }
 
@@ -154,6 +166,7 @@ class Storm_Query_Criteria implements Storm_Query_CriteriaInterface {
    */
   public function gt_eq(string $key, $value) : self {
     $this->_clauses [] = Storm_Query_Clause::greaterEqual($key, $value);
+
     return $this;
   }
 
@@ -163,6 +176,7 @@ class Storm_Query_Criteria implements Storm_Query_CriteriaInterface {
    */
   public function lt(string $key, $value) : self {
     $this->_clauses [] = Storm_Query_Clause::lesser($key, $value);
+
     return $this;
   }
 
@@ -172,12 +186,134 @@ class Storm_Query_Criteria implements Storm_Query_CriteriaInterface {
    */
   public function lt_eq(string $key, $value) : self {
     $this->_clauses [] = Storm_Query_Clause::lesserEqual($key, $value);
+
+    return $this;
+  }
+
+
+  public function eq_length(string $key, int $length) : self {
+    $this->_clauses [] = Storm_Query_Clause::equal((new Storm_Query_ClauseKey($key))
+                                                   ->beLength(),
+                                                   $length);
+
+    return $this;
+  }
+
+
+  public function not_eq_length(string $key, int $length) : self {
+    $this->_clauses [] = Storm_Query_Clause::equal((new Storm_Query_ClauseKey($key))
+                                                   ->beLength(),
+                                                   $length)
+      ->setNegated(true);
+
+    return $this;
+  }
+
+
+  public function gt_length(string $key, int $length) : self {
+    $this->_clauses [] =
+      Storm_Query_Clause::greater((new Storm_Query_ClauseKey($key))
+                                  ->beLength(),
+                                  $length);
+
+    return $this;
+  }
+
+
+  public function gt_eq_length(string $key, int $length) : self {
+    $this->_clauses [] =
+      Storm_Query_Clause::greaterEqual((new Storm_Query_ClauseKey($key))
+                                       ->beLength(),
+                                       $length);
+
+    return $this;
+  }
+
+
+  public function lt_length(string $key, int $length) : self {
+    $this->_clauses [] =
+      Storm_Query_Clause::lesser((new Storm_Query_ClauseKey($key))
+                                 ->beLength(),
+                                 $length);
+
+    return $this;
+  }
+
+
+  public function lt_eq_length(string $key, int $length) : self {
+    $this->_clauses [] =
+      Storm_Query_Clause::lesserEqual((new Storm_Query_ClauseKey($key))
+                                      ->beLength(),
+                                      $length);
+
+    return $this;
+  }
+
+
+  public function eq_left(string $key, int $width, string $value) : self {
+    $this->_clauses [] =
+      Storm_Query_Clause::equal((new Storm_Query_ClauseKey($key))
+                                ->setLeft($width),
+                                $value);
+
+    return $this;
+  }
+
+
+  public function not_eq_left(string $key, int $width, string $value) : self {
+    $this->_clauses [] =
+      Storm_Query_Clause::equal((new Storm_Query_ClauseKey($key))
+                                ->setLeft($width),
+                                $value)
+      ->setNegated(true);
+
+    return $this;
+  }
+
+
+  public function gt_left(string $key, int $width, string $value) : self {
+    $this->_clauses [] =
+      Storm_Query_Clause::greater((new Storm_Query_ClauseKey($key))
+                                  ->setLeft($width),
+                                  $value);
+
+    return $this;
+  }
+
+
+  public function gt_eq_left(string $key, int $width, string $value) : self {
+    $this->_clauses [] =
+      Storm_Query_Clause::greaterEqual((new Storm_Query_ClauseKey($key))
+                                       ->setLeft($width),
+                                       $value);
+
+    return $this;
+  }
+
+
+  public function lt_left(string $key, int $width, string $value) : self {
+    $this->_clauses [] =
+      Storm_Query_Clause::lesser((new Storm_Query_ClauseKey($key))
+                                 ->setLeft($width),
+                                 $value);
+
+    return $this;
+  }
+
+
+  public function lt_eq_left(string $key, int $width, string $value) : self {
+    $this->_clauses [] =
+      Storm_Query_Clause::lesserEqual((new Storm_Query_ClauseKey($key))
+                                      ->setLeft($width),
+                                      $value);
+
     return $this;
   }
 
 
   public function is_null(string $key) : self {
     $this->_clauses [] = Storm_Query_Clause::isNull($key);
+
     return $this;
   }
 
@@ -185,31 +321,42 @@ class Storm_Query_Criteria implements Storm_Query_CriteriaInterface {
   public function not_is_null(string $key) : self {
     $this->_clauses [] = Storm_Query_Clause::isNull($key)
       ->setNegated(true);
+
     return $this;
   }
 
 
-  public function in(string $key, array $array) : self {
-    $this->_clauses [] = Storm_Query_Clause::in($key, $array);
+  /**
+   * @param $array_or_subquery array
+   */
+  public function in(string $key, $array_or_subquery) : self {
+    $this->_clauses [] = Storm_Query_Clause::in($key, $array_or_subquery);
+
     return $this;
   }
 
 
-  public function not_in(string $key, array $array) : self {
-    $this->_clauses [] = Storm_Query_Clause::in($key, $array)
+  /**
+   * @param $array_or_subquery array
+   */
+  public function not_in(string $key, $array_or_subquery) : self {
+    $this->_clauses [] = Storm_Query_Clause::in($key, $array_or_subquery)
       ->setNegated(true);
+
     return $this;
   }
 
 
   public function start(string $key, string $value) : self {
     $this->_clauses [] = Storm_Query_Clause::start($key, $value);
+
     return $this;
   }
 
 
   public function end(string $key, string $value) : self {
     $this->_clauses [] = Storm_Query_Clause::end($key, $value);
+
     return $this;
   }
 
@@ -228,6 +375,7 @@ class Storm_Query_Criteria implements Storm_Query_CriteriaInterface {
 
   public function match(Storm_Query_MatchBoolean $match) : self {
     $this->_clauses [] = Storm_Query_Clause::match($match);
+
     return $this;
   }
 }
diff --git a/src/Storm/Query/CriteriaInterface.php b/src/Storm/Query/CriteriaInterface.php
index 4f44e2981efcf35723e6f09d2fd516144faf2d3c..3371faf47b2f807712761275473c0f62ab40e9dc 100644
--- a/src/Storm/Query/CriteriaInterface.php
+++ b/src/Storm/Query/CriteriaInterface.php
@@ -78,10 +78,16 @@ interface Storm_Query_CriteriaInterface {
   public function not_is_null(string $key) : self;
 
 
-  public function in(string $key, array $array) : self;
+  /**
+   * @param $array_or_subquery array | Storm_Query_In
+   */
+  public function in(string $key, $array_or_subquery) : self;
 
 
-  public function not_in(string $key, array $array) : self;
+  /**
+   * @param $array_or_subquery array | Storm_Query_In
+   */
+  public function not_in(string $key, $array_or_subquery) : self;
 
 
   public function start(string $key, string $value) : self;
@@ -97,4 +103,40 @@ interface Storm_Query_CriteriaInterface {
 
 
   public function match(Storm_Query_MatchBoolean $match) : self;
+
+
+  public function eq_length(string $key, int $length) : self;
+
+
+  public function not_eq_length(string $key, int $length) : self;
+
+
+  public function gt_length(string $key, int $length) : self;
+
+
+  public function gt_eq_length(string $key, int $length) : self;
+
+
+  public function lt_length(string $key, int $length) : self;
+
+
+  public function lt_eq_length(string $key, int $length) : self;
+
+
+  public function eq_left(string $key, int $width, string $value) : self;
+
+
+  public function not_eq_left(string $key, int $width, string $value) : self;
+
+
+  public function gt_left(string $key, int $width, string $value) : self;
+
+
+  public function gt_eq_left(string $key, int $width, string $value) : self;
+
+
+  public function lt_left(string $key, int $width, string $value) : self;
+
+
+  public function lt_eq_left(string $key, int $width, string $value) : self;
 }
diff --git a/src/Storm/Query/Event.php b/src/Storm/Query/Event.php
new file mode 100644
index 0000000000000000000000000000000000000000..82189ef539ea7a20e08d45454072c35e31114658
--- /dev/null
+++ b/src/Storm/Query/Event.php
@@ -0,0 +1,45 @@
+<?php
+/*
+STORM is under the MIT License (MIT)
+
+Copyright (c) 2010-2021 Agence Française Informatique http://www.afi-sa.fr
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+*/
+
+
+class Storm_Query_Event extends Storm_Event_Abstract {
+
+  protected Storm_Query_Abstract $_query;
+
+  public function __construct(Storm_Query_Abstract $query) {
+    $this->_query = $query;
+  }
+
+
+  public function getQuery() : Storm_Query_Abstract {
+    return $this->_query;
+  }
+
+
+  public function isQueryEvent() {
+    return true;
+  }
+}
diff --git a/src/Storm/Query/Exists.php b/src/Storm/Query/Exists.php
new file mode 100644
index 0000000000000000000000000000000000000000..d2605456e35fa865bb81ac763f8afbbf6e1f403a
--- /dev/null
+++ b/src/Storm/Query/Exists.php
@@ -0,0 +1,130 @@
+<?php
+/*
+  STORM is under the MIT License (MIT)
+
+  Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+  THE SOFTWARE.
+
+*/
+
+
+class Storm_Query_Exists extends Storm_Query_Sub implements Storm_Query_SubInterface {
+
+  public function sql() : string {
+    $this->getHelper()->addSelect([new Zend_Db_Expr("'x'")]);
+
+    $operator = Storm_Query_Clause::CLAUSE_EXISTS;
+    if ($this->_is_not)
+      $operator = trim(Storm_Query_Clause::CLAUSE_NOT . $operator);
+
+    return sprintf($operator . ' (%s)', $this->assembleDb());
+  }
+
+
+  public function computeErrors() : array {
+    $errors = [];
+
+    if ($this->getHelper()->hasSelect())
+      $errors [] = 'Error: For Exists don\'t add select column';
+
+    if ( ! $this->_condition)
+      $errors [] = 'Error: For Exists condition "on" is mandatory';
+
+    return [...$errors, ...parent::computeErrors()];
+  }
+
+
+  public function on_eq(string $parent_key, string $key) : self {
+    $this->_addCondition(Storm_Query_Condition::newWith($this, $parent_key, $key));
+
+    return $this;
+  }
+
+
+  public function on_not_eq(string $parent_key, string $key) : self {
+    $this->_addCondition(Storm_Query_Condition::newWith($this, $parent_key, $key)
+                         ->beNotEqual());
+
+    return $this;
+  }
+
+
+  public function on_gt(string $parent_key, string $key) : self {
+    $this->_addCondition(Storm_Query_Condition::newWith($this, $parent_key, $key)
+                         ->beGreater());
+
+    return $this;
+  }
+
+
+  public function on_gt_eq(string $parent_key, string $key) : self {
+    $this->_addCondition(Storm_Query_Condition::newWith($this, $parent_key, $key)
+                         ->beGreaterEqual());
+
+    return $this;
+  }
+
+
+  public function on_lt(string $parent_key, string $key) : self {
+    $this->_addCondition(Storm_Query_Condition::newWith($this, $parent_key, $key)
+                         ->beLesser());
+
+    return $this;
+  }
+
+
+  public function on_lt_eq(string $parent_key, string $key) : self {
+    $this->_addCondition(Storm_Query_Condition::newWith($this, $parent_key, $key)
+                         ->beLesserEqual());
+
+    return $this;
+  }
+
+
+  protected function _sqlWhere() : string {
+    $sql = new Storm_Query_Sql;
+
+    $conditions_sqls = $this->_condition
+      ? $this->_condition->sql()
+      : [];
+    foreach ($conditions_sqls as $condition_sql)
+      $sql->write($condition_sql);
+
+    $this->_criteria->assemble($sql);
+
+    foreach ($this->getSubQueries() as $sub)
+      $sql->write($sub->sql());
+
+    return $sql->implode($this->_criteria->separator());
+  }
+
+
+  protected function _addCondition(Storm_Query_Condition $condition) : self {
+    if ($this->_condition) {
+      $this->_condition->add($condition);
+
+      return $this;
+    }
+
+    $this->_condition = $condition;
+
+    return $this;
+  }
+}
diff --git a/src/Storm/Query/Helper.php b/src/Storm/Query/Helper.php
new file mode 100644
index 0000000000000000000000000000000000000000..4219e243ab36a7241fc69e210e15542cf86bf0e8
--- /dev/null
+++ b/src/Storm/Query/Helper.php
@@ -0,0 +1,183 @@
+<?php
+/*
+  STORM is under the MIT License (MIT)
+
+  Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+  THE SOFTWARE.
+
+*/
+
+
+class Storm_Query_Helper {
+
+  protected Storm_Model_Loader $_loader;
+  protected string $_alias;
+  protected string $_table_name;
+  protected string $_identifier;
+  protected ?array $_select = null;
+  protected bool $_select_with_alias = false;
+  protected bool $_is_row = false;
+
+  public function __construct(Storm_Model_Loader $loader, ?string $alias = null) {
+    $this->_loader = $loader;
+    $this->_table_name = $loader->getTable()->getName();
+    $this->_alias = $alias ?? $this->_table_name;
+    $this->_identifier = Zend_Db_Table_Abstract::getDefaultAdapter()
+      ->quoteIdentifier($this->_alias, true);
+  }
+
+
+  public function addSelect(array $select) : self {
+    $this->_select = $select;
+
+    return $this;
+  }
+
+
+  public function hasSelect() : bool {
+    return null !== $this->_select;
+  }
+
+
+  public function countSelect() : int {
+    return $this->_select ? count($this->_select) : 0;
+  }
+
+
+  public function beRow() : self {
+    $this->_is_row = true;
+
+    return $this;
+  }
+
+
+  public function allSelect() : self {
+    $this->_select = [];
+
+    return $this;
+  }
+
+
+  public function beWithAlias() : self {
+    if ($this->_select)
+      $this->_select_with_alias = true;
+
+    return $this;
+  }
+
+
+  public function isWithAlias() : bool {
+    return $this->_select_with_alias;
+  }
+
+
+  public function getAlias() : string {
+    return $this->_alias;
+  }
+
+
+  public function getIdentifier() : string {
+    return $this->_identifier;
+  }
+
+
+  public function getLoader() : Storm_Model_Loader {
+    return $this->_loader;
+  }
+
+
+  public function getTableName() : string {
+    return $this->_table_name;
+  }
+
+
+  public function getFirstSelect() : string {
+    return $this->_select
+      ? $this->_select[0] ?? ''
+      : '';
+  }
+
+
+  public function table() : array {
+    return [$this->_alias => $this->_table_name];
+  }
+
+
+  public function loaderForFetch() : Storm_Model_BaseLoader {
+    return $this->_is_row
+      ? Storm_Model_Row::getLoader()
+      : $this->getLoader();
+  }
+
+
+  /* @return = null | string | array */
+  public function select() {
+    if (null === $this->_select)
+      return null;
+
+    if (0 === count($this->_select))
+      return Zend_Db_Select::SQL_WILDCARD;
+
+    if ( ! $this->_select_with_alias)
+      return $this->_select;
+
+    $selects = [];
+    foreach ($this->_select as $value)
+      $selects [$this->_alias . '_' . $value] = $value;
+
+    return $selects;
+  }
+
+
+  public function selectDefault() : array {
+    if (null === $this->_select)
+      return [];
+
+    if (0 === count($this->_select))
+      return [];
+
+    if ( ! $this->_select_with_alias) {
+      $selects = [];
+      foreach ($this->_select as $value)
+        $selects [$value] = null;
+
+      return $selects;
+    }
+
+    $selects = [];
+    foreach ($this->_select as $value)
+      $selects [$this->_alias . '_' . $value] = null;
+
+    return $selects;
+  }
+
+
+  public function hasKeyInSelect(string $key) : bool {
+    if ( ! $this->hasSelect())
+      return false;
+
+    return $this->_isAllSelect() || in_array($key, $this->_select);
+  }
+
+
+  protected function _isAllSelect() : bool {
+    return 0 === count($this->_select);
+  }
+}
diff --git a/src/Storm/Query/In.php b/src/Storm/Query/In.php
new file mode 100644
index 0000000000000000000000000000000000000000..70b75aab1ded14a3001198251f9fd412ce381d13
--- /dev/null
+++ b/src/Storm/Query/In.php
@@ -0,0 +1,67 @@
+<?php
+/*
+  STORM is under the MIT License (MIT)
+
+  Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+  THE SOFTWARE.
+
+*/
+
+
+class Storm_Query_In extends Storm_Query_Sub {
+
+  public function sql() : string {
+    return ($parent_key = $this->_parentKey())
+      ? Storm_Query_Clause::newFor(new Storm_Query_ClauseKey($parent_key),
+                                   Storm_Query_Clause::CLAUSE_IN,
+                                   [$this->assembleDb()])
+      ->setNegated($this->_is_not)
+      ->setAlias($this->getParentHelper()->getIdentifier())
+      ->getFormatDb(false)
+      : '';
+  }
+
+
+  public function computeErrors() : array {
+    $errors = [];
+
+    if (0 === ($count = $this->getHelper()->countSelect()))
+      $errors [] = 'Error: For In add select column is mandatory';
+
+    if ($count > 1)
+      $errors [] = 'Error: For In add select must have only one column';
+
+    return [...$errors, ...parent::computeErrors()];
+  }
+
+
+  public function setCondition(Storm_Query_Condition $condition) : self {
+    $this->_condition = $condition->beIn();
+
+    return $this;
+  }
+
+
+  protected function _parentKey() : string {
+    return $this->_condition
+      ? $this->_condition->getParentKey()
+      : '';
+  }
+}
diff --git a/src/Storm/Query/Instance.php b/src/Storm/Query/Instance.php
new file mode 100644
index 0000000000000000000000000000000000000000..5ba974db548db224074a974f7073026b2edd4f04
--- /dev/null
+++ b/src/Storm/Query/Instance.php
@@ -0,0 +1,100 @@
+<?php
+/*
+  STORM is under the MIT License (MIT)
+
+  Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+  THE SOFTWARE.
+
+*/
+
+
+class Storm_Query_Instance {
+
+  protected array $_instances = [];
+
+  public function addWith(string $alias, array $instance) : self {
+    $this->_instances [$alias] = $instance;
+
+    return $this;
+  }
+
+
+  public function addFrom(Storm_Query_Instance $query_instance) : self {
+    $this->_instances = array_merge($this->_instances, $query_instance->getInstances());
+
+    return $this;
+  }
+
+
+  public function getInstances() : array {
+    return $this->_instances;
+  }
+
+
+  public function value(string $alias, string $key) {
+    return $this->_instances[$alias][$key] ?? null;
+  }
+
+
+  public function result(Storm_Query_Abstract $query, array $default_instance) : array {
+    $result_instance = [];
+
+    foreach ($this->_instances as $alias => $instance) {
+      $helper = $this->findHelperBy($alias, $query);
+      $result_instance = array_merge($result_instance,
+                                     $this->_instanceWith($instance, $helper));
+    }
+
+    return array_merge($default_instance, $result_instance);
+  }
+
+
+  public function findHelperBy(string $alias, Storm_Query_Abstract $query) : ?Storm_Query_Helper {
+    if ($alias === $query->getHelper()->getAlias())
+      return $query->getHelper();
+
+    foreach ($query->getSubQueries() as $sub_query)
+      if ($helper = $this->findHelperBy($alias, $sub_query))
+        return $helper;
+
+    foreach ($query->getJoins() as $join)
+      if ($helper = $this->findHelperBy($alias, $join))
+        return $helper;
+
+    return null;
+  }
+
+
+  protected function _instanceWith(array $instance, ?Storm_Query_Helper $helper) : array {
+    if (!$helper)
+      return [];
+
+    $alias = '';
+    if ($helper->isWithAlias())
+      $alias = $helper->getAlias() . '_';
+
+    $alias_instance = [];
+    foreach ($instance as $k => $v)
+      if ($helper->hasKeyInSelect($k))
+        $alias_instance [$alias . $k] = $v;
+
+    return $alias_instance;
+  }
+}
diff --git a/src/Storm/Query/Instances.php b/src/Storm/Query/Instances.php
new file mode 100644
index 0000000000000000000000000000000000000000..ba2f73377d7bbb9336dc8653b40af75ffe4db611
--- /dev/null
+++ b/src/Storm/Query/Instances.php
@@ -0,0 +1,106 @@
+<?php
+/*
+  STORM is under the MIT License (MIT)
+
+  Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+  THE SOFTWARE.
+
+*/
+
+
+class Storm_Query_Instances {
+
+  const INSTANCE_SEPARATOR = '@@';
+  protected array $_instances = [];
+
+  public function __construct(Storm_Query_Abstract $query) {
+    $this->_init($query);
+  }
+
+
+  protected function _init(Storm_Query_Abstract $query) : self {
+    foreach ($query->instances() as $alias => $instances)
+      $this->_instances [$alias] = $this->_addInstance($alias, $instances);
+
+    return $this;
+  }
+
+
+  public function addAll(array $searchs_alias, Storm_Collection $collection) : self {
+    $all_alias = [];
+    foreach ($searchs_alias as $search_alias)
+      $all_alias [] = $this->_aliasKey($search_alias);
+
+    $final_alias = [];
+    foreach ($all_alias as $alias) {
+      unset($this->_instances[$alias]);
+      $final_alias = [...$final_alias, ...explode(static::INSTANCE_SEPARATOR, $alias)];
+    }
+
+    $this->_instances [implode(static::INSTANCE_SEPARATOR,
+                               array_unique(array_filter($final_alias)))] = $collection;
+    return $this;
+  }
+
+
+  public function instancesFor(string $search_alias) : Storm_Collection {
+    return ($alias = $this->_aliasKey($search_alias))
+      ? $this->_instances[$alias]
+      : new Storm_Collection;
+  }
+
+
+  public function resultFor(Storm_Query_Abstract $query) : array {
+    $default_instance = $this->defaultInstance($query);
+
+    return $this->instancesFor($query->getHelper()->getAlias())
+                ->collect(fn($instance) => $instance->result($query, $default_instance))
+                ->getArrayCopy();
+  }
+
+
+  public function defaultInstance(Storm_Query_Abstract $query) : array {
+    $default = $query->getHelper()->selectDefault();
+
+    foreach ($query->getJoins() as $join)
+      $default = array_merge($default, $this->defaultInstance($join));
+
+    return $default;
+  }
+
+
+  protected function _addInstance(string $alias, array $instances) : Storm_Collection {
+    $collection = new Storm_Collection;
+
+    foreach ($instances as $instance)
+      $collection->add((new Storm_Query_Instance)->addWith($alias, $instance));
+
+    return $collection;
+  }
+
+
+  protected function _aliasKey(string $search_alias) : string {
+    foreach ($this->_instances as $alias => $collection)
+      if (in_array($search_alias, explode(static::INSTANCE_SEPARATOR, $alias)))
+        return $alias;
+
+    return '';
+  }
+}
diff --git a/src/Storm/Query/Order.php b/src/Storm/Query/Order.php
new file mode 100644
index 0000000000000000000000000000000000000000..71f3545c6fa5168a0a5de32bc091253f9aafd8d8
--- /dev/null
+++ b/src/Storm/Query/Order.php
@@ -0,0 +1,73 @@
+<?php
+/*
+  STORM is under the MIT License (MIT)
+
+  Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+  THE SOFTWARE.
+
+*/
+
+
+class Storm_Query_Order {
+
+  protected ?Storm_Query_Order $_next_query;
+  protected ?Storm_Query_Clause $_clause;
+  protected int $_position;
+
+  public function __construct(array $orders, ?int $position = 0) {
+    $this->_position = $position++;
+    $this->_clause = array_shift($orders);
+
+    $this->_next_query = ($orders
+                          ? (new Storm_Query_Order($orders, $position))
+                          : null);
+  }
+
+
+  public function getNextQuery() : ?Storm_Query_Order {
+    if (!($next_query = $this->_next_query))
+      return null;
+
+    return $next_query->getClause()
+      ? $next_query
+      : null;
+  }
+
+
+  public function getClause() : ?Storm_Query_Clause {
+    return $this->_clause;
+  }
+
+
+  public function getPosition() : int {
+    return 10 ** $this->_position;
+  }
+
+
+  public function compareVolatile(array $models) : array {
+    if (!$models)
+      return $models;
+
+    if ($clause = $this->getClause())
+      usort($models, fn($a, $b) => $clause->compare($this, $a, $b));
+
+    return $models;
+  }
+}
diff --git a/src/Storm/Query/Sql.php b/src/Storm/Query/Sql.php
index 53b347978859740a6b0c070175a3c3e208c80a99..bd9b7797787deaa990bc92479be88c918280366c 100644
--- a/src/Storm/Query/Sql.php
+++ b/src/Storm/Query/Sql.php
@@ -31,6 +31,7 @@ class Storm_Query_Sql {
 
   public function write(string $sql) : self {
     $this->_sql [] = $sql;
+
     return $this;
   }
 
diff --git a/src/Storm/Query/Sub.php b/src/Storm/Query/Sub.php
new file mode 100644
index 0000000000000000000000000000000000000000..04da101de8df7733f3ed6d54d87f38efc114bd22
--- /dev/null
+++ b/src/Storm/Query/Sub.php
@@ -0,0 +1,84 @@
+<?php
+/*
+  STORM is under the MIT License (MIT)
+
+  Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+  THE SOFTWARE.
+
+*/
+
+
+class Storm_Query_Sub extends Storm_Query_Abstract {
+
+  protected bool $_is_not = false;
+  protected ?Storm_Query_Helper $_parent_helper = null;
+  protected ?Storm_Query_Condition $_condition = null;
+
+  public function __construct(Storm_Model_Loader $loader, ?string $alias = null) {
+    $this->_helper = new Storm_Query_Helper($loader, $alias);
+    $this->_criteria = (new Storm_Query_Criteria)
+      ->setAlias($this->_helper->getIdentifier());
+  }
+
+
+  public function setParentHelper(Storm_Query_Helper $parent_helper) : self {
+    $this->_parent_helper = $parent_helper;
+
+    return $this;
+  }
+
+
+  public function getParentHelper() : ?Storm_Query_Helper {
+    return $this->_parent_helper;
+  }
+
+
+  public function beNot() : self {
+    $this->_is_not = true;
+
+    return $this;
+  }
+
+
+  public function isNot() : bool {
+    return $this->_is_not;
+  }
+
+
+  public function filterWith(Storm_Query_Instances $query_instances) : self {
+    if ($this->_condition)
+      $this->_condition->filterWith($query_instances);
+
+    foreach ($this->getSubQueries() as $sub)
+      $sub->filterWith($query_instances);
+
+    return $this;
+  }
+
+
+  public function computeErrors() : array {
+    $errors = [];
+
+    foreach ($this->getSubQueries() as $sub)
+      $errors = [...$errors, ...$sub->computeErrors()];
+
+    return $errors;
+  }
+}
diff --git a/src/Storm/Query/SubInterface.php b/src/Storm/Query/SubInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..696142403249f31c529325a00ba1f5cba8355643
--- /dev/null
+++ b/src/Storm/Query/SubInterface.php
@@ -0,0 +1,46 @@
+<?php
+/*
+  STORM is under the MIT License (MIT)
+
+  Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+  THE SOFTWARE.
+
+*/
+
+
+interface Storm_Query_SubInterface {
+
+  public function on_eq(string $parent_key, string $key) : self;
+
+
+  public function on_not_eq(string $parent_key, string $key) : self;
+
+
+  public function on_gt(string $parent_key, string $key) : self;
+
+
+  public function on_gt_eq(string $parent_key, string $key) : self;
+
+
+  public function on_lt(string $parent_key, string $key) : self;
+
+
+  public function on_lt_eq(string $parent_key, string $key) : self;
+}
diff --git a/src/Storm/Query/Trait.php b/src/Storm/Query/Trait.php
new file mode 100644
index 0000000000000000000000000000000000000000..1c1d55a2e7ab4850c5b4e47792d4604fac651db3
--- /dev/null
+++ b/src/Storm/Query/Trait.php
@@ -0,0 +1,140 @@
+<?php
+/*
+  STORM is under the MIT License (MIT)
+
+  Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+  THE SOFTWARE.
+
+*/
+
+
+trait Storm_Query_Trait {
+
+  protected bool $_distinct = false;
+  protected array $_orders = [];
+  protected ?Storm_Query_Clause $_clause_group_by = null;
+
+  public function distinct(array $select) : self {
+    $this->_distinct = true;
+
+    return $this->select($select);
+  }
+
+
+  public function group(string $value) : self {
+    $this->_clause_group_by = Storm_Query_Clause::group($value);
+
+    return $this;
+  }
+
+
+  /**
+   * @param $key_or_clause string|Storm_Query_MatchRating
+   */
+  public function order($key_or_clause, ?string $value = null) : self {
+    $this->_orders [] = $this->_order($key_or_clause, $value);
+
+    return $this;
+  }
+
+
+  /**
+   * @param $key_or_clause string|Storm_Query_MatchRating
+   */
+  public function order_desc($key_or_clause, ?string $value = null) : self {
+    $this->_orders [] = $this->_order($key_or_clause, $value)
+      ->beDesc();
+
+    return $this;
+  }
+
+
+  /**
+   * @param $key_or_clause string|Storm_Query_MatchRating
+   */
+  protected function _order($key_or_clause, ?string $value = null) : Storm_Query_Clause {
+    if ($key_or_clause instanceof Storm_Query_MatchRating)
+      return Storm_Query_Clause::order($key_or_clause->getKey(),
+                                       Storm_Query_Clause::match($key_or_clause));
+
+    return Storm_Query_Clause::order($key_or_clause, $value);
+  }
+
+
+  public function fetchAll() : array {
+    return $this->_prepareHelper()
+                ->loaderForFetch()
+                ->fetchForQuery($this);
+  }
+
+
+  public function count() : int {
+    return $this->_prepareHelper()
+                ->loaderForFetch()
+                ->countForQuery($this);
+  }
+
+
+  public function fetchFirst() : ?Storm_Model_Interface {
+    return $this->_prepareHelper()
+                ->loaderForFetch()
+                ->fetchFirstForQuery($this);
+  }
+
+
+  /** DB management */
+
+  protected function _assembleGroupBy(Zend_Db_Table_Select $select) : self {
+    if ($this->_clause_group_by)
+      $this->_clause_group_by->setAlias($this->getHelper()->getAlias())->assemble($select);
+
+    return $this;
+  }
+
+
+  protected function _assembleOrders(Zend_Db_Table_Select $select) : self {
+    foreach ($this->_orders as $order)
+      $order->setAlias($this->getHelper()->getIdentifier())->assemble($select);
+
+    return $this;
+  }
+
+
+  /** Volatile management */
+
+  public function orderVolatile(array $models) : array {
+    return (new Storm_Query_Order(array_reverse($this->_orders)))
+      ->compareVolatile($models);
+  }
+
+
+  public function getGroupByValue() : string {
+    return $this->_clause_group_by
+      ? $this->_clause_group_by->getValue()
+      : '';
+  }
+
+
+  public function notify() : self {
+    Storm_Events::getInstance()->notify(new Storm_Query_Event($this));
+
+    return $this;
+  }
+}
diff --git a/src/Storm/Test/ModelTestCase.php b/src/Storm/Test/ModelTestCase.php
index bf7534a240423b308cbaa6df428239418652c421..bcc6cf968b7c13e1e447dc9dc8b47ef8921bd776 100644
--- a/src/Storm/Test/ModelTestCase.php
+++ b/src/Storm/Test/ModelTestCase.php
@@ -25,51 +25,26 @@ THE SOFTWARE.
 */
 
 abstract class Storm_Test_ModelTestCase extends PHPUnit_Framework_TestCase {
-  use Storm_Test_THelpers;
+  use Storm_Test_THelpers, Storm_Test_TQueryTests;
 
-  protected $_old_adapter;
   protected bool $_storm_mock_zend_adapter = true;
 
   protected function setUp() {
     Storm_Model_Abstract::unsetLoaders();
 
-    $this->_old_adapter = Zend_Db_Table_Abstract::getDefaultAdapter();
-
-    if (!$this->_storm_mock_zend_adapter)
-      return;
-
-    $adapter = new class() extends Zend_Db_Adapter_Mysqli
-    {
-      public function __construct($config=[]) { }
-
-
-      public function getConfig() {
-        return ['dbname' => 'mockdb'];
-      }
-
-
-      protected function _connect() {
-        $this->_connection = Storm_Test_ObjectWrapper::mock()
-          ->whenCalled('real_escape_string')
-          ->willDo(fn($value) => $value);
-
-        return true;
-      }
-    };
-
-    Zend_Db_Table_Abstract::setDefaultAdapter($adapter);
+    $this->_querySetUp();
   }
 
 
   protected function tearDown() {
     Storm_Model_Abstract::unsetLoaders();
-    Zend_Db_Table_Abstract::setDefaultAdapter($this->_old_adapter);
+    $this->_queryTearDown();
   }
 
 
   public function assertJSONEquals($expectedJSON, $actualJSON, $message='') {
     $this->assertEquals(json_decode($expectedJSON),
-                            json_decode($actualJSON),
-                            $message);
+                        json_decode($actualJSON),
+                        $message);
   }
 }
diff --git a/src/Storm/Test/QueriesObserver.php b/src/Storm/Test/QueriesObserver.php
new file mode 100644
index 0000000000000000000000000000000000000000..d2cb8dbd4875f4ea0cee705b6aa38d1824012a10
--- /dev/null
+++ b/src/Storm/Test/QueriesObserver.php
@@ -0,0 +1,127 @@
+<?php
+/*
+STORM is under the MIT License (MIT)
+
+Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+*/
+
+
+class Storm_Test_QueriesObserver {
+
+  protected Storm_Collection $_queries;
+  protected Storm_Set $_scopes;
+
+  public function __construct() {
+    $this->_queries = new Storm_Collection;
+    $this->_scopes = new Storm_Set;
+  }
+
+
+  public function addScope(string $scope) : self {
+    $this->_scopes->add($scope);
+
+    return $this;
+  }
+
+
+  public function __invoke(Storm_Event_Abstract $event) : self {
+    if ($this->_scopes->isEmpty() || !$event->isQueryEvent())
+      return $this;
+
+    ob_start();
+    debug_print_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
+    $stack = ob_get_contents();
+    ob_end_clean();
+
+    if ($this->_scopes->detect(fn($scope) => false !== strpos($stack, $scope)))
+      $this->_addQuery($event->getQuery());
+
+    return $this;
+  }
+
+
+  public function detectWith(string $sql, ?int $count = null) : ?Storm_Test_QueriesObserverSql {
+    return $this->_queries
+      ->detect(fn($observer) => $sql === $observer->getSql()
+                                && (null === $count || $count === $observer->getCount()));
+  }
+
+
+  public function queriesMessages() : array {
+    return $this->_queries
+      ->collect(fn($observer) => $observer->getSql()
+                . sprintf(' / Called %d time(s)', $observer->getCount()))
+      ->getArrayCopy();
+  }
+
+
+  protected function _addQuery(Storm_Query_Abstract $query) : self {
+    $query_observer = new Storm_Test_QueriesObserverSql($query);
+
+    $old_observer = $this->_queries
+      ->detect(fn($observer) => $observer->getKey() === $query_observer->getKey());
+
+    if (!$old_observer) {
+      $this->_queries->add($query_observer);
+      $old_observer = $query_observer;
+    }
+
+    $old_observer->increment();
+    return $this;
+  }
+}
+
+
+
+
+class Storm_Test_QueriesObserverSql {
+
+  protected string $_sql = '';
+  protected string $_key = '';
+  protected int $_count = 0;
+
+  public function __construct(Storm_Query_Abstract $query) {
+    $this->_sql = $query->assembleVolatile();
+    $this->_key = md5($this->_sql);
+  }
+
+
+  public function getKey() : string {
+    return $this->_key;
+  }
+
+
+  public function getSql() : string {
+    return $this->_sql;
+  }
+
+
+  public function getCount() : int {
+    return $this->_count;
+  }
+
+
+  public function increment() : self {
+    $this->_count++;
+    return $this;
+  }
+}
diff --git a/src/Storm/Test/TQueryTests.php b/src/Storm/Test/TQueryTests.php
new file mode 100644
index 0000000000000000000000000000000000000000..19c2bffc8319b57db51f49f570c688de3c3f1504
--- /dev/null
+++ b/src/Storm/Test/TQueryTests.php
@@ -0,0 +1,145 @@
+<?php
+/*
+STORM is under the MIT License (MIT)
+
+Copyright (c) 2010-2022 Agence Française Informatique http://www.afi-sa.fr
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+
+trait Storm_Test_TQueryTests {
+
+  protected $_old_adapter;
+  protected array $_storm_scopes = [];
+  protected ?Storm_Test_QueriesObserver $_storm_queries_observer = null;
+
+  protected function _querySetUp() : void {
+    $this->_old_adapter = Zend_Db_Table_Abstract::getDefaultAdapter();
+
+    if (!$this->_storm_mock_zend_adapter)
+      return;
+
+    $adapter = new class() extends Zend_Db_Adapter_Mysqli {
+
+      public function __construct($config=[]) { }
+
+
+      public function getConfig() {
+        return ['dbname' => 'mockdb'];
+      }
+
+
+      protected function _connect() {
+        $this->_connection = Storm_Test_ObjectWrapper::mock()
+          ->whenCalled('real_escape_string')
+          ->willDo(fn($value) => $value);
+
+        return true;
+      }
+    };
+
+    Zend_Db_Table_Abstract::setDefaultAdapter($adapter);
+
+    $cache_core = new class() extends Zend_Cache_Core {
+
+      public function load($id,
+                           $doNotTestCacheValidity = false,
+                           $doNotUnserialize = false) {
+        return ['CLEF' => ['SCHEMA_NAME' => null,
+                           'TABLE_NAME' => 'mock_table',
+                           'COLUMN_NAME' => 'CLEF',
+                           'COLUMN_POSITION' => 1,
+                           'DATA_TYPE' => 'varchar',
+                           'DEFAULT' => null,
+                           'NULLABLE' => false,
+                           'LENGTH' => 100,
+                           'SCALE' => null,
+                           'PRECISION' => null,
+                           'UNSIGNED' => null,
+                           'PRIMARY' => true,
+                           'PRIMARY_POSITION' => 1,
+                           'IDENTITY' => false]
+        ];
+      }
+    };
+
+    Zend_Db_Table_Abstract::setDefaultMetadataCache($cache_core);
+    Storm_Model_Row::beVolatile();
+
+    $this->_registerScopes();
+  }
+
+
+  protected function _queryTearDown() : void {
+    Zend_Db_Table_Abstract::setDefaultAdapter($this->_old_adapter);
+    Zend_Db_Table_Abstract::setDefaultMetadataCache(null);
+  }
+
+
+  public function assertSql(string $sql) : void {
+    $this->assertNotNull($this->_queriesObserver()->detectWith($sql),
+                         $this->_queryMessage($sql));
+  }
+
+
+  public function assertSqlCount(string $sql, int $count) : void {
+    $this->assertNotNull($this->_queriesObserver()->detectWith($sql, $count),
+                         $this->_queryMessage($sql));
+  }
+
+
+  public function assertNotSql(string $sql) : void {
+    $this->assertNull($this->_queriesObserver()->detectWith($sql),
+                      $this->_queryMessage($sql, false));
+  }
+
+
+  protected function _queriesObserver() : Storm_Test_QueriesObserver {
+    return $this->_storm_queries_observer
+      ? $this->_storm_queries_observer
+      : ($this->_storm_queries_observer = new Storm_Test_QueriesObserver);
+  }
+
+
+  protected function _queryMessage(string $sql, ?bool $negated = true) : string {
+    $queries = $this->_queriesObserver()->queriesMessages();
+    $queries = $queries ? $queries : ['No result'];
+
+    return sprintf("There's %s match for this query:\n - %s\nIN:\n + %s\n",
+                   $negated ? 'no' : 'a',
+                   $sql,
+                   implode("\n + ", $queries));
+  }
+
+
+  protected function _registerScopes() : self {
+    if ( ! $this->_storm_scopes)
+      return $this;
+
+    $this->_storm_queries_observer = new Storm_Test_QueriesObserver;
+
+    foreach ($this->_storm_scopes as $scope)
+      $this->_storm_queries_observer->addScope($scope);
+
+    Storm_Events::getInstance()->register($this->_storm_queries_observer);
+
+    return $this;
+  }
+}
diff --git a/tests/Storm/Model/TableReconnectTest.php b/tests/Storm/Model/TableReconnectTest.php
index ac0a4663a0f90c1131c774b6cb870781cf243003..9fa345043e7600f31a1ad90acc36fe9738a7b6e5 100644
--- a/tests/Storm/Model/TableReconnectTest.php
+++ b/tests/Storm/Model/TableReconnectTest.php
@@ -25,7 +25,9 @@ THE SOFTWARE.
 */
 
 class Storm_Model_TableReconnectTest extends Storm_Test_ModelTestCase {
+
   protected $_mock;
+  protected bool $_storm_mock_zend_adapter = false;
 
   public function setUp() {
     parent::setUp();
@@ -182,4 +184,4 @@ class Storm_Model_TableReconnectTest_Adapter extends Zend_Db_Adapter_Mysqli {
 
 class Storm_Model_TableReconnectTest_Model extends Storm_Model_Abstract {
   protected $_table_name = 'reconnect_test';
-}
\ No newline at end of file
+}
diff --git a/tests/Storm/Test/LoaderQueryTest.php b/tests/Storm/Test/LoaderQueryTest.php
index 1a3464fe985493f178cdcc88e10200d94533a101..9aa6bc6c08e8723154095ed6e589ed19218889fd 100644
--- a/tests/Storm/Test/LoaderQueryTest.php
+++ b/tests/Storm/Test/LoaderQueryTest.php
@@ -24,491 +24,556 @@
 */
 
 
-abstract class Storm_Test_LoaderQueryTestCase extends Storm_Test_ModelTestCase {
+class Storm_Test_LoaderQueryVolatileClauseWhereTest
+  extends Storm_Test_ModelTestCase {
 
-  protected
-    $_select,
-    $_table,
-    $_loader;
+  protected array $_storm_scopes = ['LoaderQueryVolatileClauseWhereTest'];
 
   public function setUp() {
     parent::setUp();
 
-    $this->_table = $this->mock();
-    $this->_loader = Storm_Test_Mock_User::getLoader()->setTable($this->_table);
-  }
-}
-
-
-
-
-class Storm_Test_LoaderQueryDbTest extends Storm_Test_LoaderQueryTestCase {
-
-  public function setUp() {
-    parent::setUp();
+    $this->fixture(Storm_Test_LoaderQueryUser::class,
+                   ['id' => 100,
+                    'login' => 'user_admin',
+                    'level' => 'invite admin redacteur',
+                    'foo' => 'premier deuxieme troisieme',
+                    'prefs' => null,
+                    'sys' => '',
+                    'date' => '2022-01-01']);
 
-    $this->_table
-      ->whenCalled('select')->answers($this->_select = $this->mock())
-      ->whenCalled('fetchAll')->with($this->_select)
-      ->answers(new Zend_Db_Table_Rowset([]));
+    $this->fixture(Storm_Test_LoaderQueryUser::class,
+                   ['id' => 101,
+                    'login' => 'user_administrateur',
+                    'level' => 'administrateur',
+                    'foo' => 'deuxieme redacteur',
+                    'prefs' => '{"best":true}',
+                    'sys' => null,
+                    'date' => '2020-01-01']);
 
-    $this->_select->whenCalled('where')->answers($this->_select)
-                  ->whenCalled('getTable')->answers($this->_table);
+    $this->fixture(Storm_Test_LoaderQueryUser::class,
+                   ['id' => 102,
+                    'login' => 'user_invite',
+                    'level' => 'invite',
+                    'foo' => 'premier deuxieme',
+                    'url' => 'https://my-server.org/page_1.html',
+                    'website' => 'https://my-server.org/pomme+d\'api.php page2.php',
+                    'sys' => 'content',
+                    'date' => '2021-01-01']);
   }
 
 
   /** @test */
-  public function withClauseGreaterThanShouldReturnQuery() {
-    Storm_Query::from(Storm_Test_Mock_User::class)
-      ->gt('id', '30')
-      ->fetchAll();
-
-    $this->assertEquals(['id>\'30\''],
-                        $this->_select->getAttributesForLastCallOn('where'));
+  public function whereId_GT_100_ShouldAnswersUserAdministrateurAndUserInvite() {
+    $this->assertEquals(['user_administrateur', 'user_invite'],
+                        (new Storm_Model_Collection(Storm_Test_LoaderQueryUser::query()
+                                                    ->gt('id', 100)
+                                                    ->fetchAll()))
+                        ->collect('login')
+                        ->getArrayCopy());
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE (`users`.`id` > 100)');
   }
 
 
   /** @test */
-  public function withClauseGreaterThanEqualShouldReturnQuery() {
-    Storm_Query::from(Storm_Test_Mock_User::class)
-      ->gt_eq('id', '30')
-      ->fetchAll();
-
-    $this->assertEquals(['id>=\'30\''],
-                        $this->_select->getAttributesForLastCallOn('where'));
+  public function whereId_GT_EQ_101_ShouldAnswersUserAdministrateurAndUserInvite() {
+    $this->assertEquals(['user_administrateur', 'user_invite'],
+                        (new Storm_Model_Collection(Storm_Test_LoaderQueryUser::query()
+                                                    ->gt_eq('id', 101)
+                                                    ->fetchAll()))
+                        ->collect('login')
+                        ->getArrayCopy());
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE (`users`.`id` >= 101)');
   }
 
 
   /** @test */
-  public function withClauseLesserThanShouldReturnQuery() {
-    Storm_Query::from(Storm_Test_Mock_User::class)
-      ->lt('id', '30')
-      ->fetchAll();
-
-    $this->assertEquals(['id<\'30\''],
-                        $this->_select->getAttributesForLastCallOn('where'));
+  public function whereId_LT_102_ShouldAnswersUserAdminAndUserAdministrateur() {
+    $this->assertEquals(['user_admin', 'user_administrateur'],
+                        (new Storm_Model_Collection(Storm_Test_LoaderQueryUser::query()
+                                                    ->lt('id', 102)
+                                                    ->fetchAll()))
+                        ->collect('login')
+                        ->getArrayCopy());
+    $this->assertSqlCount('SELECT `users`.* FROM `users` WHERE (`users`.`id` < 102)', 1);
   }
 
 
   /** @test */
-  public function withClauseLesserThanEqualShouldReturnQuery() {
-    Storm_Query::from(Storm_Test_Mock_User::class)
-      ->lt_eq('id', '30')
-      ->fetchAll();
-
-    $this->assertEquals(['id<=\'30\''],
-                        $this->_select->getAttributesForLastCallOn('where'));
+  public function whereId_LT_EQ_101_ShouldAnswersUserAdminAndUserAdministrateur() {
+    $this->assertEquals(['user_admin', 'user_administrateur'],
+                        (new Storm_Model_Collection(Storm_Test_LoaderQueryUser::query()
+                                                    ->lt_eq('id', 101)
+                                                    ->fetchAll()))
+                        ->collect('login')
+                        ->getArrayCopy());
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE (`users`.`id` <= 101)');
+    $this->assertNotSql('SELECT `users`.* FROM `users` WHERE (`users`.`id` > 101)');
   }
 
 
   /** @test */
-  public function withClauseIsShouldReturnQuery() {
-    Storm_Query::from(Storm_Test_Mock_User::class)
-      ->is_null('name')
-      ->fetchAll();
-
-    $this->assertEquals(['name is null'],
-                        $this->_select->getAttributesForLastCallOn('where'));
+  public function whereLogin_NOT_EQ_UserAdministrateurShouldReturnUserAdminAndUserInvite() {
+    $this->assertEquals(['user_admin', 'user_invite'],
+                        (new Storm_Model_Collection(Storm_Test_LoaderQueryUser::query()
+                                                    ->not_eq('login', 'user_administrateur')
+                                                    ->fetchAll()))
+                        ->collect('login')
+                        ->getArrayCopy());
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE (`users`.`login` != \'user_administrateur\')');
   }
 
 
   /** @test */
-  public function withClauseIsNotShouldReturnQuery() {
-    Storm_Query::from(Storm_Test_Mock_User::class)
-      ->not_is_null('name')
-      ->fetchAll();
-
-    $this->assertEquals(['name is not null'],
-                        $this->_select->getAttributesForLastCallOn('where'));
+  public function whereId_IN_100And102_AndFoo_NOT_LIKE_TroisieShouldReturnId_102() {
+    $this->assertEquals([102],
+                        (new Storm_Model_Collection(Storm_Test_LoaderQueryUser::query()
+                                                    ->in('id', [100, 102])
+                                                    ->not_like('foo', '%troisie%')
+                                                    ->fetchAll()))
+                        ->collect('id')
+                        ->getArrayCopy());
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE (`users`.`id` IN (100, 102) AND `users`.`foo` NOT LIKE \'%troisie%\')');
   }
 
 
   /** @test */
-  public function withClauseInShouldReturnQuery() {
-    Storm_Query::from(Storm_Test_Mock_User::class)
-      ->in('name', ['Harlock', 'Nausicaa'])
-      ->fetchAll();
-
-    $this->assertEquals(['name in (\'Harlock\', \'Nausicaa\')'],
-                        $this->_select->getAttributesForLastCallOn('where'));
+  public function whereId_IN_100And101_AndFoo_START_DeuxShouldReturnId_101() {
+    $this->assertEquals([101],
+                        (new Storm_Model_Collection(Storm_Test_LoaderQueryUser::query()
+                                                    ->in('id', [100, 101])
+                                                    ->start('foo', 'deux')
+                                                    ->fetchAll()))
+                        ->collect('id')
+                        ->getArrayCopy());
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE (`users`.`id` IN (100, 101) AND `users`.`foo` LIKE \'deux%\')');
   }
 
 
   /** @test */
-  public function withClauseNotInShouldReturnQuery() {
-    Storm_Query::from(Storm_Test_Mock_User::class)
-      ->not_in('name', ['Harlock', 'Nausicaa'])
-      ->fetchAll();
-
-    $this->assertEquals(['name not in (\'Harlock\', \'Nausicaa\')'],
-                        $this->_select->getAttributesForLastCallOn('where'));
+  public function whereFoo_END_ActeurShouldReturnId_101() {
+    $this->assertEquals([101],
+                        (new Storm_Model_Collection(Storm_Test_LoaderQueryUser::query()
+                                                    ->end('foo', 'acteur')
+                                                    ->fetchAll()))
+                        ->collect('id')
+                        ->getArrayCopy());
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE (`users`.`foo` LIKE \'%acteur\')');
   }
 
 
   /** @test */
-  public function withClauseEqualShouldReturnQuery() {
-    Storm_Query::from(Storm_Test_Mock_User::class)
-      ->eq('left(name, 5)', 'ganda')
-      ->fetchAll();
+  public function withMatchLevelAgainstAdminShouldAnswersOnlyUser_Admin() {
+    $query = Storm_Test_LoaderQueryUser::query()
+      ->match((new Storm_Query_MatchBoolean('level'))
+              ->against(['admin']));
 
-    $this->assertEquals(['left(name, 5)=\'ganda\''],
-                        $this->_select->getAttributesForLastCallOn('where'));
+    $this->assertEquals(['user_admin'],
+                        (new Storm_Model_Collection($query->fetchAll()))
+                        ->collect('login')
+                        ->getArrayCopy());
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE (MATCH(level) AGAINST(\'admin\' IN BOOLEAN MODE))');
   }
 
 
   /** @test */
-  public function withClauseNotEqualShouldReturnQuery() {
-    Storm_Query::from(Storm_Test_Mock_User::class)
-      ->not_eq('name', 'Harlock')
-      ->fetchAll();
+  public function withMatchLevelAgainstAdminDeuxiemeShouldAnswersUser_AdminAndUser_AdministrateurAndUser_Invite() {
+    $query = Storm_Test_LoaderQueryUser::query()
+      ->match((new Storm_Query_MatchBoolean('level,foo'))
+              ->against(['admin', 'deuxieme']));
 
-    $this->assertEquals(['name!=\'Harlock\''],
-                        $this->_select->getAttributesForLastCallOn('where'));
+    $this->assertEquals(['user_admin', 'user_administrateur', 'user_invite'],
+                        (new Storm_Model_Collection($query->fetchAll()))
+                        ->collect('login')
+                        ->getArrayCopy());
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE (MATCH(level,foo) AGAINST(\'admin deuxieme\' IN BOOLEAN MODE))');
   }
 
 
   /** @test */
-  public function withClauseLikeShouldReturnQuery() {
-    Storm_Query::from(Storm_Test_Mock_User::class)
-      ->like('login', '%aus%')
-      ->fetchAll();
+  public function withMatchLevelAgainstStrictAdminRedacteur_DeuxiemeShouldAnswersUser_AdminAndUser_Administrateur() {
+    $query = Storm_Test_LoaderQueryUser::query()
+      ->match((new Storm_Query_MatchBoolean('level,foo', true))
+              ->against(['admin', 'redacteur'])
+              ->against(['deuxieme']));
 
-    $this->assertEquals(['login like \'%aus%\''],
-                        $this->_select->getAttributesForLastCallOn('where'));
+    $this->assertEquals(['user_admin', 'user_administrateur'],
+                        (new Storm_Model_Collection($query->fetchAll()))
+                        ->collect('login')
+                        ->getArrayCopy());
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE (MATCH(level,foo) AGAINST(\'+(admin redacteur) +deuxieme\' IN BOOLEAN MODE))');
   }
 
 
   /** @test */
-  public function withClauseLikeAndSlashesShouldReturnQueryWith() {
-    Storm_Query::from(Storm_Test_Mock_User::class)
-      ->like('login', '%https://my-server.org%')
-      ->fetchAll();
+  public function withMatchLevelFooExactDeuxiemeRedacteurShouldAnswersOnlyUser_Administrateur() {
+    $query = Storm_Test_LoaderQueryUser::query()
+      ->match((new Storm_Query_MatchBoolean('level,foo'))
+              ->exact('deuxieme redacteur'));
 
-    $this->assertEquals(['login like \'%https://my-server.org%\''],
-                        $this->_select->getAttributesForLastCallOn('where'));
+    $this->assertEquals(['user_administrateur'],
+                        (new Storm_Model_Collection($query->fetchAll()))
+                        ->collect('login')
+                        ->getArrayCopy());
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE (MATCH(level,foo) AGAINST(\'"deuxieme redacteur"\' IN BOOLEAN MODE))');
   }
 
 
   /** @test */
-  public function withClauseLikeAndPlusAndQuoteShouldReturnQueryWith() {
-    Storm_Query::from(Storm_Test_Mock_User::class)
-      ->like('login', '%https://my-server.org/pomme+d\'api%')
-      ->fetchAll();
+  public function withMatchLevelFooDeuxiemeAndAgainstRedacteurAnswers_AllUsers() {
+    $query = Storm_Test_LoaderQueryUser::query()
+      ->match((new Storm_Query_MatchBoolean('level,foo'))
+              ->against('deuxieme')
+              ->against('redacteur'));
 
-    $this->assertEquals(['login like \'%https://my-server.org/pomme+d\'api%\''],
-                        $this->_select->getAttributesForLastCallOn('where'));
+    $this->assertEquals(['user_admin', 'user_administrateur', 'user_invite'],
+                        (new Storm_Model_Collection($query->fetchAll()))
+                        ->collect('login')
+                        ->getArrayCopy());
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE (MATCH(level,foo) AGAINST(\'deuxieme redacteur\' IN BOOLEAN MODE))');
   }
 
 
   /** @test */
-  public function withClauseNotLikeShouldReturnQuery() {
-    Storm_Query::from(Storm_Test_Mock_User::class)
-      ->not_like('login', '%aus%')
-      ->fetchAll();
+  public function withMatchLevelFooDeuxiemeAndAgainstStrictRedacteurAnswers_User_Admin_User_Administrateur() {
+    $query = Storm_Test_LoaderQueryUser::query()
+      ->match((new Storm_Query_MatchBoolean('level,foo'))
+              ->against('deuxieme')
+              ->against('redacteur', true));
 
-    $this->assertEquals(['login not like \'%aus%\''],
-                        $this->_select->getAttributesForLastCallOn('where'));
+    $this->assertEquals(['user_admin', 'user_administrateur'],
+                        (new Storm_Model_Collection($query->fetchAll()))
+                        ->collect('login')
+                        ->getArrayCopy());
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE (MATCH(level,foo) AGAINST(\'deuxieme +redacteur\' IN BOOLEAN MODE))');
   }
 
 
   /** @test */
-  public function withClauseStartShouldReturnQuery() {
-    Storm_Query::from(Storm_Test_Mock_User::class)
-      ->start('login', 'aus')
-      ->fetchAll();
+  public function withOrClause_LevelInviteOrFooLikePremierShouldAnswersUser_AdminAndUser_Invite() {
+    $query = Storm_Test_LoaderQueryUser::query()
+      ->or((new Storm_Query_Criteria)
+           ->eq('level', 'invite')
+           ->like('foo', '%premier%'));
 
-    $this->assertEquals(['login like \'aus%\''],
-                        $this->_select->getAttributesForLastCallOn('where'));
+    $this->assertEquals(['user_admin', 'user_invite'],
+                        (new Storm_Model_Collection($query->fetchAll()))
+                        ->collect('login')
+                        ->getArrayCopy());
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE ((`users`.`level` = \'invite\' OR `users`.`foo` LIKE \'%premier%\'))');
   }
 
 
   /** @test */
-  public function withClauseEndShouldReturnQuery() {
-    Storm_Query::from(Storm_Test_Mock_User::class)
-      ->end('login', 'aus')
-      ->fetchAll();
+  public function withAndClause_LevelInviteOrFooLikePremierShouldAnswersOnlyUser_Invite() {
+    $query = Storm_Test_LoaderQueryUser::query()
+      ->and((new Storm_Query_Criteria)
+            ->eq('level', 'invite')
+            ->like('foo', '%premier%'));
 
-    $this->assertEquals(['login like \'%aus\''],
-                        $this->_select->getAttributesForLastCallOn('where'));
+    $this->assertEquals(['user_invite'],
+                        (new Storm_Model_Collection($query->fetchAll()))
+                        ->collect('login')
+                        ->getArrayCopy());
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE ((`users`.`level` = \'invite\' AND `users`.`foo` LIKE \'%premier%\'))');
   }
 
 
   /** @test */
-  public function withClauseOrShouldReturnQuery() {
-    Storm_Query::from(Storm_Test_Mock_User::class)
-      ->or((new Storm_Query_Criteria)
-           ->eq('login', 'aus')
-           ->in('role', ['admin', 'invite']))
-      ->fetchAll();
+  public function urlLikeHttpsDoubleDotSlashSlashShouldAnswersOnlyUserWithSlashes() {
+    $query = Storm_Test_LoaderQueryUser::query()
+      ->like('url', '%https://my-server.org%');
 
-    $this->assertEquals(['(login=\'aus\' or role in (\'admin\', \'invite\'))'],
-                        $this->_select->getAttributesForLastCallOn('where'));
+    $this->assertEquals(['user_invite'],
+                        (new Storm_Model_Collection($query->fetchAll()))
+                        ->collect('login')
+                        ->getArrayCopy());
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE (`users`.`url` LIKE \'%https://my-server.org%\')');
   }
 
 
   /** @test */
-  public function withClauseAndShouldReturnQuery() {
-    Storm_Query::from(Storm_Test_Mock_User::class)
-      ->and((new Storm_Query_Criteria)
-            ->eq('login', 'aus')
-            ->in('role', ['admin', 'invite']))
-      ->fetchAll();
+  public function websiteLikeHttpsDoubleDotSlashSlashPlusQuotePommeShouldAnswersOnlyUserWithSlashesPlusAndQuote() {
+    $query = Storm_Test_LoaderQueryUser::query()
+      ->like('website', '%https://my-server.org/pomme+d\'api%');
 
-    $this->assertEquals(['(login=\'aus\' and role in (\'admin\', \'invite\'))'],
-                        $this->_select->getAttributesForLastCallOn('where'));
+    $this->assertEquals(['user_invite'],
+                        (new Storm_Model_Collection($query->fetchAll()))
+                        ->collect('login')
+                        ->getArrayCopy());
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE (`users`.`website` LIKE \'%https://my-server.org/pomme+d\'api%\')');
   }
 
 
   /** @test */
-  public function withClauseMatchShouldReturnQueryInBooleanMode() {
-    Storm_Query::from(Storm_Test_Mock_User::class)
-      ->match((new Storm_Query_MatchBoolean('login, role'))
-              ->against(['ADMIN', 'INVITE']))
-      ->fetchAll();
+  public function urlMatchHttpsDoubleDotSlashSlashShouldAnswersOnlyUserWithSlashes() {
+    $query = Storm_Test_LoaderQueryUser::query()
+      ->match((new Storm_Query_MatchBoolean('website'))->against('https://my-server.org/pomme+d\'api.php'));
 
-    $this->assertEquals(['MATCH(login, role) AGAINST(\'ADMIN INVITE\' IN BOOLEAN MODE)'],
-                        $this->_select->getAttributesForLastCallOn('where'));
+    $this->assertEquals(['user_invite'],
+                        (new Storm_Model_Collection($query->fetchAll()))
+                        ->collect('login')
+                        ->getArrayCopy());
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE (MATCH(website) AGAINST(\'https://my-server.org/pomme+d\'api.php\' IN BOOLEAN MODE))');
   }
 
 
   /** @test */
-  public function withClauseMatchMultipleTermsShouldReturnQueryInBooleanMode() {
-    Storm_Query::from(Storm_Test_Mock_User::class)
-      ->match((new Storm_Query_MatchBoolean('login,role'))
-              ->against('ADMIN INVITE')
-              ->against('HUGO ADRIEN'))
-      ->fetchAll();
+  public function withIsNullPrefsShouldAnswersUser_100_Only() {
+    $query = Storm_Test_LoaderQueryUser::query()
+      ->is_null('prefs');
 
-    $this->assertEquals(['MATCH(login,role) AGAINST(\'ADMIN INVITE HUGO ADRIEN\' IN BOOLEAN MODE)'],
-                        $this->_select->getAttributesForLastCallOn('where'));
+    $this->assertEquals([Storm_Test_LoaderQueryUser::find(100)],
+                        $query->fetchAll());
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE (`users`.`prefs` IS null)');
   }
 
 
   /** @test */
-  public function withClauseMatchStrictAndOneExactExpressionWithBooleanModeShouldReturnQuery() {
-    Storm_Query::from(Storm_Test_Mock_User::class)
-      ->match((new Storm_Query_MatchBoolean(['login', 'role'], true))
-              ->exact('ADMIN INVITE')
-              ->against('HUGO'))
-      ->fetchAll();
+  public function withIsNotNullPrefsShouldAnswersUsers_101_and_102() {
+    $query = Storm_Test_LoaderQueryUser::query()
+      ->not_is_null('prefs');
 
-    $this->assertEquals(['MATCH(login, role) AGAINST(\'+"ADMIN INVITE" +HUGO\' IN BOOLEAN MODE)'],
-                        $this->_select->getAttributesForLastCallOn('where'));
+    $this->assertEquals([Storm_Test_LoaderQueryUser::find(101),
+                         Storm_Test_LoaderQueryUser::find(102)],
+                        $query->fetchAll());
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE (`users`.`prefs` IS NOT null)');
   }
 
 
   /** @test */
-  public function withClauseMatchOneExactAndOneAgainstStrictInBooleanModeShouldReturnQuery() {
-    Storm_Query::from(Storm_Test_Mock_User::class)
-      ->match((new Storm_Query_MatchBoolean(['login', 'role']))
-              ->exact('ADMIN INVITE', true)
-              ->against('HUGO'))
-      ->fetchAll();
+  public function withNotInInviteAdministrateurShouldAnswerUser_100_Only() {
+    $query = Storm_Test_LoaderQueryUser::query()
+      ->not_in('level', ['invite', 'administrateur']);
 
-    $this->assertEquals(['MATCH(login, role) AGAINST(\'+"ADMIN INVITE" HUGO\' IN BOOLEAN MODE)'],
-                        $this->_select->getAttributesForLastCallOn('where'));
+    $this->assertEquals([Storm_Test_LoaderQueryUser::find(100)],
+                        $query->fetchAll());
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE (`users`.`level` NOT IN (\'invite\', \'administrateur\'))');
   }
 
 
   /** @test */
-  public function withClauseMatchOneExactStrictAndOneAgainstInBooleanModeShouldReturnQuery() {
-    Storm_Query::from(Storm_Test_Mock_User::class)
-      ->match((new Storm_Query_MatchBoolean(['login', 'role']))
-              ->exact('ADMIN INVITE')
-              ->against('HUGO', true))
-      ->fetchAll();
+  public function withClauseLimitOneInIntAndStringShouldReturnOnlyUser_100() {
+    $this->assertEquals([Storm_Test_LoaderQueryUser::find(100)],
+                        Storm_Test_LoaderQueryUser::query()
+                        ->limit(1)
+                        ->fetchAll());
 
-    $this->assertEquals(['MATCH(login, role) AGAINST(\'"ADMIN INVITE" +HUGO\' IN BOOLEAN MODE)'],
-                        $this->_select->getAttributesForLastCallOn('where'));
+    $this->assertSql('SELECT `users`.* FROM `users` LIMIT 1');
   }
-}
-
 
 
+  /** @test */
+  public function withEqualLengthOnColumnLevelShouldAnswerUser_102_Only() {
+    $query = Storm_Test_LoaderQueryUser::query()
+      ->eq_length('level', 6);
 
-class Storm_Test_LoaderQueryLimitTest extends Storm_Test_LoaderQueryTestCase {
-
-  public function setUp() {
-    parent::setUp();
-
-    $this->_table
-      ->whenCalled('select')->answers($this->_select = $this->mock())
-      ->whenCalled('fetchAll')->with($this->_select)
-      ->answers(new Zend_Db_Table_Rowset([]));
-
-    $this->_select->whenCalled('limit')->answers($this->_select);
+    $this->assertEquals([Storm_Test_LoaderQueryUser::find(102)],
+                        $query->fetchAll());
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE (LENGTH(`users`.`level`) = 6)');
   }
 
 
   /** @test */
-  public function withClauseLimit30ShouldReturnQuery() {
-    Storm_Query::from(Storm_Test_Mock_User::class)
-      ->limit(30)
-      ->fetchAll();
+  public function withNotEqualLengthOnColumnLevelShouldAnswerUser_100_101() {
+    $query = Storm_Test_LoaderQueryUser::query()
+      ->not_eq_length('level', 6);
 
-    $this->assertEquals([30],
-                        $this->_select->getAttributesForLastCallOn('limit'));
+    $this->assertEquals([Storm_Test_LoaderQueryUser::find(100),
+                         Storm_Test_LoaderQueryUser::find(101)],
+                        $query->fetchAll());
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE (LENGTH(`users`.`level`) != 6)');
   }
 
 
   /** @test */
-  public function withClauseLimit30Offset10ShouldReturnQuery() {
-    Storm_Query::from(Storm_Test_Mock_User::class)
-      ->limit('10, 30')
-      ->fetchAll();
+  public function withGreaterLengthOnColumnLevelShouldAnswerUser_100_Only() {
+    $query = Storm_Test_LoaderQueryUser::query()
+      ->gt_length('level', 14);
 
-    $this->assertEquals(['10, 30'],
-                        $this->_select->getAttributesForLastCallOn('limit'));
+    $this->assertEquals([Storm_Test_LoaderQueryUser::find(100)],
+                        $query->fetchAll());
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE (LENGTH(`users`.`level`) > 14)');
   }
-}
-
 
 
+  /** @test */
+  public function withGreaterEqualLengthOnColumnLevelShouldAnswerUser_100_101() {
+    $query = Storm_Test_LoaderQueryUser::query()
+      ->gt_eq_length('level', 14);
 
-class Storm_Test_LoaderQueryLimitPageTest extends Storm_Test_LoaderQueryTestCase {
+    $this->assertEquals([Storm_Test_LoaderQueryUser::find(100),
+                         Storm_Test_LoaderQueryUser::find(101)],
+                        $query->fetchAll());
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE (LENGTH(`users`.`level`) >= 14)');
+  }
 
-  public function setUp() {
-    parent::setUp();
 
-    $this->_table
-      ->whenCalled('select')->answers($this->_select = $this->mock())
-      ->whenCalled('fetchAll')->with($this->_select)
-      ->answers(new Zend_Db_Table_Rowset([]));
+  /** @test */
+  public function withLesserLengthOnColumnLevelShouldAnswerUser_102_Only() {
+    $query = Storm_Test_LoaderQueryUser::query()
+      ->lt_length('level', 14);
 
-    $this->_select->whenCalled('limitPage')->answers($this->_select);
+    $this->assertEquals([Storm_Test_LoaderQueryUser::find(102)],
+                        $query->fetchAll());
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE (LENGTH(`users`.`level`) < 14)');
   }
 
 
   /** @test */
-  public function withClauseLimitPageShouldReturnQuery() {
-    Storm_Query::from(Storm_Test_Mock_User::class)
-      ->limit_page([1, 100])
-      ->fetchAll();
+  public function withLesserEqualLengthOnColumnLevelShouldAnswerUser_101_102() {
+    $query = Storm_Test_LoaderQueryUser::query()
+      ->lt_eq_length('level', 14);
 
-    $this->assertEquals([1, 100],
-                        $this->_select->getAttributesForLastCallOn('limitPage'));
+    $this->assertEquals([Storm_Test_LoaderQueryUser::find(101),
+                         Storm_Test_LoaderQueryUser::find(102)],
+                        $query->fetchAll());
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE (LENGTH(`users`.`level`) <= 14)');
   }
-}
-
 
 
+  /** @test */
+  public function withEqualLeftWidth4OnColumnDateShouldAnswerUser_101_Only() {
+    $query = Storm_Test_LoaderQueryUser::query()
+      ->eq_left('date', 4, '2020');
 
-class Storm_Test_LoaderQueryOrderTest extends Storm_Test_LoaderQueryTestCase {
+    $this->assertEquals([Storm_Test_LoaderQueryUser::find(101)],
+                        $query->fetchAll());
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE (LEFT(`users`.`date`, 4) = \'2020\')');
+  }
 
-  public function setUp() {
-    parent::setUp();
 
-    $this->_table
-      ->whenCalled('select')->answers($this->_select = $this->mock())
-      ->whenCalled('fetchAll')->with($this->_select)
-      ->answers(new Zend_Db_Table_Rowset([]));
+  /** @test */
+  public function withNotEqualLeftWidth4OnColumnDateShouldAnswerUser_100_102() {
+    $query = Storm_Test_LoaderQueryUser::query()
+      ->not_eq_left('date', 4, '2020');
 
-    $this->_select->whenCalled('order')->answers($this->_select)
-                  ->whenCalled('getTable')->answers($this->_table);
+    $this->assertEquals([Storm_Test_LoaderQueryUser::find(100),
+                         Storm_Test_LoaderQueryUser::find(102)],
+                        $query->fetchAll());
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE (LEFT(`users`.`date`, 4) != \'2020\')');
   }
 
 
   /** @test */
-  public function withClauseOrderIdShouldReturnOrderId() {
-    Storm_Query::from(Storm_Test_Mock_User::class)
-      ->order('id')
-      ->fetchAll();
+  public function withGreaterLeftWidth4OnColumnDateShouldAnswerUser_100_Only() {
+    $query = Storm_Test_LoaderQueryUser::query()
+      ->gt_left('date', 4, '2021');
 
-    $this->assertEquals(['id'],
-                        $this->_select->getAttributesForLastCallOn('order'));
+    $this->assertEquals([Storm_Test_LoaderQueryUser::find(100)],
+                        $query->fetchAll());
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE (LEFT(`users`.`date`, 4) > \'2021\')');
   }
 
 
   /** @test */
-  public function withClauseOrderIdDescShouldReturnOrderIdDesc() {
-    Storm_Query::from(Storm_Test_Mock_User::class)
-      ->order_desc('id')
-      ->fetchAll();
+  public function withGreaterEqualLeftColumnDateShouldAnswerUser_100_102() {
+    $query = Storm_Test_LoaderQueryUser::query()
+      ->gt_eq_left('date', 4, '2021');
 
-    $this->assertEquals(['id desc'],
-                        $this->_select->getAttributesForLastCallOn('order'));
+    $this->assertEquals([Storm_Test_LoaderQueryUser::find(100),
+                         Storm_Test_LoaderQueryUser::find(102)],
+                        $query->fetchAll());
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE (LEFT(`users`.`date`, 4) >= \'2021\')');
   }
 
 
   /** @test */
-  public function withMultipleClauseOrderShouldReturnAllOrders() {
-    Storm_Query::from(Storm_Test_Mock_User::class)
-      ->order_desc('id')
-      ->order('name')
-      ->order_desc('role')
-      ->fetchAll();
+  public function withLesserLeftColumnDateShouldAnswerUser_101_Only() {
+    $query = Storm_Test_LoaderQueryUser::query()
+      ->lt_left('date', 4, '2021');
 
-    $this->assertEquals(['id desc', 'name', 'role desc'],
-                        [$this->_select->getFirstAttributeForMethodCallAt('order', 0),
-                         $this->_select->getFirstAttributeForMethodCallAt('order', 1),
-                         $this->_select->getFirstAttributeForMethodCallAt('order', 2)]);
+    $this->assertEquals([Storm_Test_LoaderQueryUser::find(101)],
+                        $query->fetchAll());
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE (LEFT(`users`.`date`, 4) < \'2021\')');
   }
 
 
   /** @test */
-  public function withMatchClauseOrderShouldReturnMatchOrder() {
-    Storm_Query::from(Storm_Test_Mock_User::class)
-      ->order((new Storm_Query_MatchRating('login, role'))
-              ->against(['ADMIN', 'INVITE']))
-      ->fetchAll();
+  public function withLesserEqualLeftColumnDateShouldAnswerUser_101_102() {
+    $query = Storm_Test_LoaderQueryUser::query()
+      ->lt_eq_left('date', 4, '2021');
 
-    $this->assertEquals(['MATCH(login, role) AGAINST(\'ADMIN INVITE\')'],
-                        $this->_select->getAttributesForLastCallOn('order'));
+    $this->assertEquals([Storm_Test_LoaderQueryUser::find(101),
+                         Storm_Test_LoaderQueryUser::find(102)],
+                        $query->fetchAll());
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE (LEFT(`users`.`date`, 4) <= \'2021\')');
   }
 
 
   /** @test */
-  public function withMatchClauseOrderDescShouldReturnMatchOrder() {
-    Storm_Query::from(Storm_Test_Mock_User::class)
-      ->order_desc((new Storm_Query_MatchBoolean('login, role'))
-                   ->against(['ADMIN', 'INVITE']))
-      ->fetchAll();
+  public function withClauseLimitPageShouldPaginate() {
+    $this->assertEquals([Storm_Test_LoaderQueryUser::find(100),
+                         Storm_Test_LoaderQueryUser::find(101)],
+                        Storm_Test_LoaderQueryUser::query()
+                        ->limit_page([0, 2])
+                        ->fetchAll());
+    $this->assertEquals([Storm_Test_LoaderQueryUser::find(100),
+                         Storm_Test_LoaderQueryUser::find(101)],
+                        Storm_Test_LoaderQueryUser::query()
+                        ->limit_page([1, 2])
+                        ->fetchAll());
+    $this->assertEquals([Storm_Test_LoaderQueryUser::find(102)],
+                        Storm_Test_LoaderQueryUser::query()
+                        ->limit_page([2, 2])
+                        ->fetchAll());
 
-    $this->assertEquals(['MATCH(login, role) AGAINST(\'ADMIN INVITE\' IN BOOLEAN MODE) desc'],
-                        $this->_select->getAttributesForLastCallOn('order'));
+    $this->assertSqlCount('SELECT `users`.* FROM `users` LIMIT 2',
+                          2);
+    $this->assertSqlCount('SELECT `users`.* FROM `users` LIMIT 2 OFFSET 2',
+                          1);
   }
-}
-
-
-
-
-class Storm_Test_LoaderQueryGroupByTest extends Storm_Test_LoaderQueryTestCase {
-
-  public function setUp() {
-    parent::setUp();
 
-    $this->_table
-      ->whenCalled('select')->answers($this->_select = $this->mock())
-      ->whenCalled('fetchAll')->with($this->_select)
-      ->answers(new Zend_Db_Table_Rowset([]));
 
-    $this->_select->whenCalled('group')->answers($this->_select);
+  /** @test */
+  public function withClauseLimitPageAndAttributeShouldWork() {
+    $this->assertEquals([Storm_Test_LoaderQueryUser::find(100),
+                         Storm_Test_LoaderQueryUser::find(102)],
+                        Storm_Test_LoaderQueryUser::query()
+                        ->start('foo', 'premier')
+                        ->limit_page([1, 2])
+                        ->fetchAll());
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE (`users`.`foo` LIKE \'premier%\') LIMIT 2');
   }
 
 
   /** @test */
-  public function withGroupByLoaderShouldCallGroupOnDbSelect() {
-    Storm_Query::from(Storm_Test_Mock_User::class)
-      ->group('name')
-      ->fetchAll();
-
-    $this->assertEquals(['name'],
-                        $this->_select->getAttributesForLastCallOn('group'));
-  }
-}
-
+  public function queriesObserverShouldDisplaySqlAndCallCount() {
+    foreach (range(1, 2) as $range)
+      Storm_Test_LoaderQueryUser::query()
+        ->eq('id', 100)
+        ->fetchAll();
 
+    foreach (range(1, 5) as $range)
+      Storm_Test_LoaderQueryUser::query()
+        ->gt('id', 100)
+        ->fetchAll();
 
+    Storm_Test_LoaderQueryUser::query()
+      ->eq('id', 100)
+      ->fetchAll();
 
-class Storm_Test_LoaderQueryUser extends Storm_Model_Abstract {
+    foreach (range(1, 4) as $range)
+      Storm_Test_LoaderQueryUser::query()
+        ->in('id', [100, 102])
+        ->gt('id', 100)
+        ->fetchAll();
 
-  protected $_table_name = 'users';
+    $this->assertSqlCount('SELECT `users`.* FROM `users` WHERE (`users`.`id` = 100)',
+                          3);
+    $this->assertSqlCount('SELECT `users`.* FROM `users` WHERE (`users`.`id` > 100)',
+                          5);
+    $this->assertSqlCount('SELECT `users`.* FROM `users` WHERE (`users`.`id` IN (100, 102) AND `users`.`id` > 100)',
+                          4);
+  }
 }
 
 
 
 
-class Storm_Test_LoaderQueryVolatileClauseWhereTest extends Storm_Test_ModelTestCase {
+class Storm_Test_LoaderQueryVolatileClauseOrderTest
+  extends Storm_Test_ModelTestCase {
+
+  protected array $_storm_scopes = ['LoaderQueryVolatileClauseOrderTest'];
 
   public function setUp() {
     parent::setUp();
@@ -517,479 +582,1032 @@ class Storm_Test_LoaderQueryVolatileClauseWhereTest extends Storm_Test_ModelTest
                    ['id' => 100,
                     'login' => 'user_admin',
                     'level' => 'invite admin redacteur',
-                    'foo' => 'premier deuxieme troisieme',
-                    'prefs' => null]);
+                    'foo' => 'first second third',
+                    'order1' => 'abcd',
+                    'order2' => 2,
+                    'order3' => 'defg',
+                    'order4' => 'weburl']);
 
     $this->fixture(Storm_Test_LoaderQueryUser::class,
                    ['id' => 101,
                     'login' => 'user_administrateur',
                     'level' => 'administrateur',
-                    'foo' => 'deuxieme redacteur',
-                    'prefs' => '{"best":true}']);
+                    'foo' => 'third second forth',
+                    'order1' => 'abcdef',
+                    'order2' => 3,
+                    'order3' => 'defg',
+                    'order4' => 'no']);
 
     $this->fixture(Storm_Test_LoaderQueryUser::class,
                    ['id' => 102,
                     'login' => 'user_invite',
                     'level' => 'invite',
-                    'foo' => 'premier deuxieme',
-                    'url' => 'https://my-server.org/page_1.html',
-                    'website' => 'https://my-server.org/pomme+d\'api.php page2.php']);
-  }
-
-
-  /** @test */
-  public function whereId_GT_100_ShouldAnswersUserAdministrateurAndUserInvite() {
-    $this->assertEquals(['user_administrateur', 'user_invite'],
-                        (new Storm_Model_Collection(Storm_Query::from(Storm_Test_LoaderQueryUser::class)
-                                                    ->gt('id', 100)
-                                                    ->fetchAll()))
-                        ->collect('login')
-                        ->getArrayCopy());
+                    'foo' => 'forth fifth seven',
+                    'order1' => 'cdef',
+                    'order2' => 1,
+                    'order3' => 'klmn',
+                    'order4' => '']);
   }
 
 
   /** @test */
-  public function whereId_GT_EQ_101_ShouldAnswersUserAdministrateurAndUserInvite() {
-    $this->assertEquals(['user_administrateur', 'user_invite'],
-                        (new Storm_Model_Collection(Storm_Query::from(Storm_Test_LoaderQueryUser::class)
-                                                    ->gt_eq('id', 101)
-                                                    ->fetchAll()))
-                        ->collect('login')
-                        ->getArrayCopy());
-  }
-
+  public function with_Order1Desc_ShouldAnswersInOrdered() {
+    $query = Storm_Test_LoaderQueryUser::query()
+      ->order_desc('order1');
 
-  /** @test */
-  public function whereId_LT_102_ShouldAnswersUserAdminAndUserAdministrateur() {
-    $this->assertEquals(['user_admin', 'user_administrateur'],
-                        (new Storm_Model_Collection(Storm_Query::from(Storm_Test_LoaderQueryUser::class)
-                                                    ->lt('id', 102)
-                                                    ->fetchAll()))
+    $this->assertEquals(['user_invite', 'user_administrateur', 'user_admin'],
+                        (new Storm_Model_Collection($query->fetchAll()))
                         ->collect('login')
                         ->getArrayCopy());
+    $this->assertSql('SELECT `users`.* FROM `users` ORDER BY `users`.`order1` DESC');
   }
 
 
   /** @test */
-  public function whereId_LT_EQ_101_ShouldAnswersUserAdminAndUserAdministrateur() {
-    $this->assertEquals(['user_admin', 'user_administrateur'],
-                        (new Storm_Model_Collection(Storm_Query::from(Storm_Test_LoaderQueryUser::class)
-                                                    ->lt_eq('id', 101)
-                                                    ->fetchAll()))
-                        ->collect('login')
-                        ->getArrayCopy());
-  }
-
+  public function with_Order3Asc_Order2Desc_ShouldAnswersOrderedUser() {
+    $query = Storm_Test_LoaderQueryUser::query()
+      ->order('order3')
+      ->order_desc('order2');
 
-  /** @test */
-  public function whereLogin_NOT_EQ_UserAdministrateurShouldReturnUserAdminAndUserInvite() {
-    $this->assertEquals(['user_admin', 'user_invite'],
-                        (new Storm_Model_Collection(Storm_Query::from(Storm_Test_LoaderQueryUser::class)
-                                                    ->not_eq('login', 'user_administrateur')
-                                                    ->fetchAll()))
+    $this->assertEquals(['user_administrateur', 'user_admin', 'user_invite'],
+                        (new Storm_Model_Collection($query->fetchAll()))
                         ->collect('login')
                         ->getArrayCopy());
+    $this->assertSql('SELECT `users`.* FROM `users` ORDER BY `users`.`order3` ASC, `users`.`order2` DESC');
   }
 
 
   /** @test */
-  public function whereId_IN_100And102_AndFoo_NOT_LIKE_TroisieShouldReturnId_102() {
-    $this->assertEquals([102],
-                        (new Storm_Model_Collection(Storm_Query::from(Storm_Test_LoaderQueryUser::class)
-                                                    ->in('id', [100, 102])
-                                                    ->not_like('foo', '%troisie%')
-                                                    ->fetchAll()))
-                        ->collect('id')
-                        ->getArrayCopy());
-  }
-
-
-  /** @test */
-  public function whereId_IN_100And101_AndFoo_START_DeuxShouldReturnId_101() {
-    $this->assertEquals([101],
-                        (new Storm_Model_Collection(Storm_Query::from(Storm_Test_LoaderQueryUser::class)
-                                                    ->in('id', [100, 101])
-                                                    ->start('foo', 'deux')
-                                                    ->fetchAll()))
-                        ->collect('id')
-                        ->getArrayCopy());
-  }
-
-
-  /** @test */
-  public function whereFoo_END_ActeurShouldReturnId_101() {
-    $this->assertEquals([101],
-                        (new Storm_Model_Collection(Storm_Query::from(Storm_Test_LoaderQueryUser::class)
-                                                    ->end('foo', 'acteur')
-                                                    ->fetchAll()))
-                        ->collect('id')
-                        ->getArrayCopy());
-  }
-
+  public function with_Order3Desc_Order2Desc_ShouldAnswersOrderedUser() {
+    $query = Storm_Test_LoaderQueryUser::query()
+      ->order_desc('order3')
+      ->order_desc('order2');
 
-  /** @test */
-  public function withMatchLevelAgainstAdminShouldAnswersOnlyUser_Admin() {
-    $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class)
-      ->match((new Storm_Query_MatchBoolean('level'))
-              ->against(['admin']));
-    $this->assertEquals(['user_admin'],
+    $this->assertEquals(['user_invite', 'user_administrateur', 'user_admin'],
                         (new Storm_Model_Collection($query->fetchAll()))
                         ->collect('login')
                         ->getArrayCopy());
+    $this->assertSql('SELECT `users`.* FROM `users` ORDER BY `users`.`order3` DESC, `users`.`order2` DESC');
   }
 
 
   /** @test */
-  public function withMatchLevelAgainstAdminDeuxiemeShouldAnswersUser_AdminAndUser_AdministrateurAndUser_Invite() {
-    $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class)
-      ->match((new Storm_Query_MatchBoolean('level,foo'))
-              ->against(['admin', 'deuxieme']));
-    $this->assertEquals(['user_admin', 'user_administrateur', 'user_invite'],
-                        (new Storm_Model_Collection($query->fetchAll()))
-                        ->collect('login')
-                        ->getArrayCopy());
-  }
-
+  public function withMatch_Level_Administrateur_ShouldAnswersOrderedUser() {
+    $query = Storm_Test_LoaderQueryUser::query()
+      ->order((new Storm_Query_MatchRating('level'))
+              ->against('administrateur'));
 
-  /** @test */
-  public function withMatchLevelAgainstStrictAdminRedacteur_DeuxiemeShouldAnswersUser_AdminAndUser_Administrateur() {
-    $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class)
-      ->match((new Storm_Query_MatchBoolean('level,foo', true))
-              ->against(['admin', 'redacteur'])
-              ->against(['deuxieme']));
-    $this->assertEquals(['user_admin', 'user_administrateur'],
+    $this->assertEquals(['user_admin', 'user_invite', 'user_administrateur'],
                         (new Storm_Model_Collection($query->fetchAll()))
                         ->collect('login')
                         ->getArrayCopy());
+    $this->assertSql('SELECT `users`.* FROM `users` ORDER BY MATCH(level) AGAINST(\'administrateur\') ASC');
   }
 
 
   /** @test */
-  public function withMatchLevelFooExactDeuxiemeRedacteurShouldAnswersOnlyUser_Administrateur() {
-    $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class)
-      ->match((new Storm_Query_MatchBoolean('level,foo'))
-              ->exact('deuxieme redacteur'));
-    $this->assertEquals(['user_administrateur'],
+  public function withMatch_Level_Administrateur_Redacteur_Desc_ShouldAnswersOrderedUser() {
+    $query = Storm_Test_LoaderQueryUser::query()
+      ->order_desc((new Storm_Query_MatchRating('level'))
+                   ->against('administrateur redacteur'));
+
+    $this->assertEquals(['user_administrateur', 'user_admin', 'user_invite'],
                         (new Storm_Model_Collection($query->fetchAll()))
                         ->collect('login')
                         ->getArrayCopy());
+    $this->assertSql('SELECT `users`.* FROM `users` ORDER BY MATCH(level) AGAINST(\'administrateur redacteur\') DESC');
   }
 
 
   /** @test */
-  public function withMatchLevelFooDeuxiemeAndAgainstRedacteurAnswers_AllUsers() {
-    $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class)
-      ->match((new Storm_Query_MatchBoolean('level,foo'))
-              ->against('deuxieme')
-              ->against('redacteur'));
+  public function withMatchInBooleanMode_Level_Administrateur_Redacteur_Desc_ShouldAnswersOrderedUser() {
+    $query = Storm_Test_LoaderQueryUser::query()
+      ->order_desc((new Storm_Query_MatchBoolean('level'))
+                   ->against('administrateur redacteur'));
+
     $this->assertEquals(['user_admin', 'user_administrateur', 'user_invite'],
                         (new Storm_Model_Collection($query->fetchAll()))
                         ->collect('login')
                         ->getArrayCopy());
+    $this->assertSql('SELECT `users`.* FROM `users` ORDER BY MATCH(level) AGAINST(\'administrateur redacteur\' IN BOOLEAN MODE) DESC');
   }
 
 
   /** @test */
-  public function withMatchLevelFooDeuxiemeAndAgainstStrictRedacteurAnswers_User_Admin_User_Administrateur() {
-    $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class)
-      ->match((new Storm_Query_MatchBoolean('level,foo'))
-              ->against('deuxieme')
-              ->against('redacteur', true));
-    $this->assertEquals(['user_admin', 'user_administrateur'],
-                        (new Storm_Model_Collection($query->fetchAll()))
-                        ->collect('login')
-                        ->getArrayCopy());
-  }
-
+  public function withMatch_FooDescExacteInBooleanMode_Forth_Fifth_Level_Administrateur_Redacteur_ShouldAnswersOrderedUser() {
+    $query = Storm_Test_LoaderQueryUser::query()
+      ->order_desc((new Storm_Query_MatchBoolean('foo'))
+                   ->exact('forth fifth'))
+      ->order((new Storm_Query_MatchRating('level'))
+              ->against('administrateur redacteur'));
 
-  /** @test */
-  public function withOrClause_LevelInviteOrFooLikePremierShouldAnswersUser_AdminAndUser_Invite() {
-    $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class)
-      ->or((new Storm_Query_Criteria)
-           ->eq('level', 'invite')
-           ->like('foo', '%premier%'));
-    $this->assertEquals(['user_admin', 'user_invite'],
+    $this->assertEquals(['user_invite', 'user_admin', 'user_administrateur'],
                         (new Storm_Model_Collection($query->fetchAll()))
                         ->collect('login')
                         ->getArrayCopy());
+    $this->assertSql('SELECT `users`.* FROM `users` ORDER BY MATCH(foo) AGAINST(\'"forth fifth"\' IN BOOLEAN MODE) DESC, MATCH(level) AGAINST(\'administrateur redacteur\') ASC');
   }
 
 
   /** @test */
-  public function withAndClause_LevelInviteOrFooLikePremierShouldAnswersOnlyUser_Invite() {
-    $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class)
-      ->and((new Storm_Query_Criteria)
-            ->eq('level', 'invite')
-            ->like('foo', '%premier%'));
-    $this->assertEquals(['user_invite'],
-                        (new Storm_Model_Collection($query->fetchAll()))
-                        ->collect('login')
-                        ->getArrayCopy());
-  }
+  public function withMatch_FooLevelStrict_InviteRedacteur_SecondThird_ShouldAnswersOrderedUser() {
+    // https://www.php.net/manual/fr/function.usort
+    // If two members compare as equal, they retain their original order. Prior to PHP 8.0.0, their relative order in the sorted array was undefined.
+    if (version_compare(PHP_VERSION, '8.0.0', '<'))
+      return;
 
+    $query = Storm_Test_LoaderQueryUser::query()
+      ->order((new Storm_Query_MatchBoolean('foo,level', true))
+              ->against('invite redacteur')
+              ->against('second third'));
 
-  /** @test */
-  public function urlLikeHttpsDoubleDotSlashSlashShouldAnswersOnlyUserWithSlashes() {
-    $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class)
-      ->like('url', '%https://my-server.org%');
-    $this->assertEquals(['user_invite'],
-                        (new Storm_Model_Collection($query->fetchAll()))
-                        ->collect('login')
-                        ->getArrayCopy());
+    $this->assertEquals([Storm_Test_LoaderQueryUser::find(101),
+                         Storm_Test_LoaderQueryUser::find(102),
+                         Storm_Test_LoaderQueryUser::find(100)],
+                        $query->fetchAll());
+    $this->assertSql('SELECT `users`.* FROM `users` ORDER BY MATCH(foo,level) AGAINST(\'+(invite redacteur) +(second third)\' IN BOOLEAN MODE) ASC');
   }
 
 
   /** @test */
-  public function websiteLikeHttpsDoubleDotSlashSlashPlusQuotePommeShouldAnswersOnlyUserWithSlashesPlusAndQuote() {
-    $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class)
-      ->like('website', '%https://my-server.org/pomme+d\'api%');
+  public function selectUsersGroupByOrder3ShouldAnswersUserAdminAndUserInvite() {
+    $query = Storm_Test_LoaderQueryUser::query()
+      ->group('order3');
 
-    $this->assertEquals(['user_invite'],
+    $this->assertEquals([0 => 'user_admin', 2 => 'user_invite'],
                         (new Storm_Model_Collection($query->fetchAll()))
                         ->collect('login')
                         ->getArrayCopy());
+    $this->assertSql('SELECT `users`.* FROM `users` GROUP BY `users`.`order3`');
   }
 
 
   /** @test */
-  public function urlMatchHttpsDoubleDotSlashSlashShouldAnswersOnlyUserWithSlashes() {
-    $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class)
-      ->match((new Storm_Query_MatchBoolean('website'))->against('https://my-server.org/pomme+d\'api.php'));
-    $this->assertEquals(['user_invite'],
-                        (new Storm_Model_Collection($query->fetchAll()))
-                        ->collect('login')
-                        ->getArrayCopy());
-  }
+  public function withSelectShouldAnswersWithRowModel() {
+    $results = Storm_Test_LoaderQueryUser::query()
+      ->select(['login', 'level'])
+      ->fetchAll();
 
+    $user = Storm_Test_LoaderQueryUser::find(100);
+    $this->assertEquals(['login' => $user->getLogin(),
+                         'level' => $user->getLevel()],
+                        $results[0]->toArray());
+    $this->assertTrue($results[0] instanceof Storm_Model_Row);
 
-  /** @test */
-  public function withIsNullPrefsShouldAnswersUser_100_Only() {
-    $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class)
-      ->and((new Storm_Query_Criteria)->is_null('prefs'));
+    $user = Storm_Test_LoaderQueryUser::find(101);
+    $this->assertEquals(['login' => $user->getLogin(),
+                         'level' => $user->getLevel()],
+                        $results[1]->toArray());
+    $this->assertTrue($results[1] instanceof Storm_Model_Row);
 
-    $this->assertEquals([Storm_Test_LoaderQueryUser::find(100)],
-                        $query->fetchAll());
+    $user = Storm_Test_LoaderQueryUser::find(102);
+    $this->assertEquals(['login' => $user->getLogin(),
+                         'level' => $user->getLevel()],
+                        $results[2]->toArray());
+    $this->assertTrue($results[2] instanceof Storm_Model_Row);
+    $this->assertSql('SELECT `users`.`login`, `users`.`level` FROM `users`');
   }
 
 
   /** @test */
-  public function withIsNotNullPrefsShouldAnswersUsers_101_and_102() {
-    $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class)
-      ->and((new Storm_Query_Criteria)->not_is_null('prefs'));
+  public function with_Order4WithNoValueDesc_ShouldAnswersOrderedUser_100_101_102() {
+    $query = Storm_Test_LoaderQueryUser::query()
+      ->order_desc('order4', 'no');
 
     $this->assertEquals([Storm_Test_LoaderQueryUser::find(101),
+                         Storm_Test_LoaderQueryUser::find(100),
                          Storm_Test_LoaderQueryUser::find(102)],
                         $query->fetchAll());
+    $this->assertSqlCount('SELECT `users`.* FROM `users` ORDER BY `users`.`order4` = \'no\' DESC', 1);
   }
+}
 
 
-  /** @test */
-  public function withNotInInviteAdministrateurShouldAnswerUser_100_Only() {
-    $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class)
-      ->not_in('level', ['invite', 'administrateur']);
 
-    $this->assertEquals([Storm_Test_LoaderQueryUser::find(100)],
-                        $query->fetchAll());
-  }
 
+class Storm_Test_LoaderQueryVolatileDistinctTest
+  extends Storm_Test_ModelTestCase {
 
-  /** @test */
-  public function withClauseLimitOneShouldReturnOnlyUser_100() {
-    $this->assertEquals([Storm_Test_LoaderQueryUser::find(100)],
-                        Storm_Query::from(Storm_Test_LoaderQueryUser::class)
-                        ->limit(1)
-                        ->fetchAll());
+  protected array $_storm_scopes = ['LoaderQueryVolatileDistinctTest'];
+
+  public function setUp() {
+    parent::setUp();
+
+    $this->fixture(Storm_Test_LoaderQueryUser::class,
+                   ['id' => 100,
+                    'distinct1' => 'abcd',
+                    'distinct2' => 1234]);
+
+    $this->fixture(Storm_Test_LoaderQueryUser::class,
+                   ['id' => 101,
+                    'distinct1' => 'abcd',
+                    'distinct2' => 1234]);
+
+    $this->fixture(Storm_Test_LoaderQueryUser::class,
+                   ['id' => 102,
+                    'distinct1' => 'abcd',
+                    'distinct2' => 4567]);
+
+    $this->fixture(Storm_Test_LoaderQueryUser::class,
+                   ['id' => 103,
+                    'distinct1' => 'zyw',
+                    'distinct2' => 987]);
+
+    $this->fixture(Storm_Test_LoaderQueryUser::class,
+                   ['id' => 104,
+                    'distinct1' => 'fed',
+                    'distinct2' => 987]);
   }
 
 
   /** @test */
-  public function withClauseLimitOneTwoShouldReturnTwoUser_101_102() {
-    $this->assertEquals([Storm_Test_LoaderQueryUser::find(101),
-                         Storm_Test_LoaderQueryUser::find(102)],
-                        Storm_Query::from(Storm_Test_LoaderQueryUser::class)
-                        ->limit('1, 2')
-                        ->fetchAll());
+  public function withDistinct1AndDistinct2ShouldAnswers4Result() {
+    $row_results = Storm_Test_LoaderQueryUser::query()
+      ->distinct(['distinct1', 'distinct2'])
+      ->fetchAll();
+
+    $this->assertEquals([['distinct1' => 'abcd', 'distinct2' => 1234],
+                         ['distinct1' => 'abcd', 'distinct2' => 4567],
+                         ['distinct1' => 'zyw', 'distinct2' => 987],
+                         ['distinct1' => 'fed', 'distinct2' => 987]],
+                        array_map(fn($row) => $row->toArray(),
+                                  $row_results));
+    $this->assertSql('SELECT DISTINCT `users`.`distinct1`, `users`.`distinct2` FROM `users`');
+  }
+}
+
+
+
+
+class Storm_Test_LoaderQueryVolatileJoinInnerTest
+  extends Storm_Test_ModelTestCase {
+
+  protected array $_storm_scopes = ['LoaderQueryVolatileJoinInnerTest'];
+
+  public function setUp() {
+    parent::setUp();
+
+    $this->fixture(Storm_Test_LoaderQueryMovie::class,
+                   ['id' => 1,
+                    'name' => 'Movie name 1',
+                    'date' => '2020']);
+    $this->fixture(Storm_Test_LoaderQueryMovie::class,
+                   ['id' => 2,
+                    'name' => 'Movie name 2',
+                    'date' => '2022']);
+    $this->fixture(Storm_Test_LoaderQueryMovie::class,
+                   ['id' => 3,
+                    'name' => 'Movie name 3',
+                    'date' => '2021']);
+    $this->fixture(Storm_Test_LoaderQueryMovie::class,
+                   ['id' => 4,
+                    'name' => 'Movie name 4',
+                    'date' => '2018']);
+    $this->fixture(Storm_Test_LoaderQueryMovie::class,
+                   ['id' => 5,
+                    'name' => 'Movie name 5',
+                    'date' => '2019']);
+
+    $this->fixture(Storm_Test_LoaderQueryActor::class,
+                   ['id' => 1,
+                    'movie_id' => 3,
+                    'name' => 'Actor name 1',
+                    'age' => 25]);
+    $this->fixture(Storm_Test_LoaderQueryActor::class,
+                   ['id' => 2,
+                    'movie_id' => 1,
+                    'name' => 'Actor name 2',
+                    'age' => 52]);
+    $this->fixture(Storm_Test_LoaderQueryActor::class,
+                   ['id' => 3,
+                    'movie_id' => 4,
+                    'name' => 'Actor name 3',
+                    'age' => 36]);
+    $this->fixture(Storm_Test_LoaderQueryActor::class,
+                   ['id' => 4,
+                    'movie_id' => 3,
+                    'name' => 'Actor name 4',
+                    'age' => 20]);
+  }
+
+
+  /** @test */
+  public function withInnerJoinShouldReturnIntersectMoviesActorsOnlyOneResult() {
+    $results = Storm_Test_LoaderQueryMovie::join('tab1')
+      ->select(['name', 'date'])
+      ->inner(Storm_Test_LoaderQueryActor::inner('tab2')
+              ->select(['name', 'age'])
+              ->on_eq('id', 'movie_id')
+              ->gt('age', 30))
+      ->in('date', ['2019', '2020', '2021'])
+      ->fetchAll();
+
+    $row = $results[0];
+    $this->assertCount(1, $results);
+    $this->assertEquals('Movie name 1', $row->getTab1Name());
+    $this->assertEquals('2020', $row->tab1_date);
+    $this->assertEquals('Actor name 2', $row->getTab2Name());
+    $this->assertEquals(52, $row->tab2_age);
+    $this->assertTrue($row instanceof Storm_Model_Row);
+
+    $this->assertSql('SELECT `tab1`.`name` AS `tab1_name`, `tab1`.`date` AS `tab1_date`, `tab2`.`name` AS `tab2_name`, `tab2`.`age` AS `tab2_age` FROM `movies` AS `tab1`
+ INNER JOIN `actors` AS `tab2` ON `tab1`.`id` = `tab2`.`movie_id` AND `tab2`.`age` > 30 WHERE (`tab1`.`date` IN (\'2019\', \'2020\', \'2021\'))');
   }
 
 
   /** @test */
-  public function withClauseLimitPageShouldPaginate() {
-    $this->assertEquals([Storm_Test_LoaderQueryUser::find(100),
-                         Storm_Test_LoaderQueryUser::find(101)],
-                        Storm_Query::from(Storm_Test_LoaderQueryUser::class)
-                        ->limit_page([0, 2])
-                        ->fetchAll());
-    $this->assertEquals([Storm_Test_LoaderQueryUser::find(100),
-                         Storm_Test_LoaderQueryUser::find(101)],
-                        Storm_Query::from(Storm_Test_LoaderQueryUser::class)
-                        ->limit_page([1, 2])
-                        ->fetchAll());
-    $this->assertEquals([Storm_Test_LoaderQueryUser::find(102)],
-                        Storm_Query::from(Storm_Test_LoaderQueryUser::class)
-                        ->limit_page([2, 2])
-                        ->fetchAll());
+  public function withInnerJoinShouldReturnIntersectDedupMovieAndTwoActors() {
+    $results = Storm_Test_LoaderQueryMovie::join('tab1')
+      ->select(['name', 'date'])
+      ->inner(Storm_Test_LoaderQueryActor::inner('tab2')
+              ->select(['name', 'age'])
+              ->on_eq('id', 'movie_id')
+              ->lt('age', 30))
+      ->in('date', ['2019', '2020', '2021'])
+      ->fetchAll();
+
+    $this->assertCount(2, $results);
+    $this->assertEquals([['tab1_name' => 'Movie name 3',
+                          'tab1_date' => '2021',
+                          'tab2_name' => 'Actor name 1',
+                          'tab2_age' => 25],
+                         ['tab1_name' => 'Movie name 3',
+                          'tab1_date' => '2021',
+                          'tab2_name' => 'Actor name 4',
+                          'tab2_age' => 20]],
+                        array_map(fn($row) => $row->toArray(),
+                                  $results));
+    $this->assertTrue($results[0] instanceof Storm_Model_Row);
+
+    $this->assertSql('SELECT `tab1`.`name` AS `tab1_name`, `tab1`.`date` AS `tab1_date`, `tab2`.`name` AS `tab2_name`, `tab2`.`age` AS `tab2_age` FROM `movies` AS `tab1`
+ INNER JOIN `actors` AS `tab2` ON `tab1`.`id` = `tab2`.`movie_id` AND `tab2`.`age` < 30 WHERE (`tab1`.`date` IN (\'2019\', \'2020\', \'2021\'))');
   }
 
 
   /** @test */
-  public function withClauseLimitPageAndAttributeShouldWork() {
-    $this->assertEquals([Storm_Test_LoaderQueryUser::find(100),
-                         Storm_Test_LoaderQueryUser::find(102)],
-                        Storm_Query::from(Storm_Test_LoaderQueryUser::class)
-                        ->start('foo', 'premier')
-                        ->limit_page([1, 2])
-                        ->fetchAll());
+  public function withInnerJoinWithoutSelectShouldReturnMovies() {
+    $results = Storm_Test_LoaderQueryMovie::join('tab1')
+      ->inner(Storm_Test_LoaderQueryActor::inner('tab2')
+              ->on_eq('id', 'movie_id')
+              ->gt('age', 30))
+      ->in('date', ['2019', '2020', '2021'])
+      ->fetchAll();
+
+    $this->assertCount(1, $results);
+    $this->assertEquals(['id' => 1,
+                         'name' => 'Movie name 1',
+                         'date' => '2020'],
+                        $results[0]->toArray());
+    $this->assertTrue($results[0] instanceof Storm_Model_Row);
+
+    $this->assertSql('SELECT `tab1`.* FROM `movies` AS `tab1`
+ INNER JOIN `actors` AS `tab2` ON `tab1`.`id` = `tab2`.`movie_id` AND `tab2`.`age` > 30 WHERE (`tab1`.`date` IN (\'2019\', \'2020\', \'2021\'))');
+  }
+
+
+  /** @test */
+  public function withInnerJoinWithSelectTab2AllShouldReturnActors() {
+    $results = Storm_Test_LoaderQueryMovie::join('tab1')
+      ->inner(Storm_Test_LoaderQueryActor::inner('tab2')
+              ->on_eq('id', 'movie_id')
+              ->select()
+              ->gt('age', 30))
+      ->in('date', ['2019', '2020', '2021'])
+      ->fetchAll();
+
+    $this->assertCount(1, $results);
+    $this->assertEquals(['id' => 2,
+                         'movie_id' => 1,
+                         'name' => 'Actor name 2',
+                         'age' => 52],
+                        $results[0]->toArray());
+    $this->assertTrue($results[0] instanceof Storm_Model_Row);
+
+    $this->assertSql('SELECT `tab2`.* FROM `movies` AS `tab1`
+ INNER JOIN `actors` AS `tab2` ON `tab1`.`id` = `tab2`.`movie_id` AND `tab2`.`age` > 30 WHERE (`tab1`.`date` IN (\'2019\', \'2020\', \'2021\'))');
   }
 }
 
 
 
 
-class Storm_Test_LoaderQueryVolatileClauseOrderTest
+class Storm_Test_LoaderQueryVolatileJoinLeftTest
+  extends Storm_Test_ModelTestCase {
+
+  protected array $_storm_scopes = ['LoaderQueryVolatileJoinLeftTest'];
+
+  public function setUp() {
+    parent::setUp();
+
+    $this->fixture(Storm_Test_LoaderQueryMovie::class,
+                   ['id' => 1,
+                    'name' => 'Movie name 1',
+                    'date' => '2020']);
+    $this->fixture(Storm_Test_LoaderQueryMovie::class,
+                   ['id' => 2,
+                    'name' => 'Movie name 2',
+                    'date' => '2022']);
+    $this->fixture(Storm_Test_LoaderQueryMovie::class,
+                   ['id' => 3,
+                    'name' => 'Movie name 3',
+                    'date' => '2021']);
+    $this->fixture(Storm_Test_LoaderQueryMovie::class,
+                   ['id' => 4,
+                    'name' => 'Movie name 4',
+                    'date' => '2018']);
+
+    $this->fixture(Storm_Test_LoaderQueryActor::class,
+                   ['id' => 1,
+                    'movie_id' => 3,
+                    'name' => 'Actor name 1',
+                    'age' => 25]);
+    $this->fixture(Storm_Test_LoaderQueryActor::class,
+                   ['id' => 2,
+                    'movie_id' => 1,
+                    'name' => 'Actor name 2',
+                    'age' => 52]);
+    $this->fixture(Storm_Test_LoaderQueryActor::class,
+                   ['id' => 3,
+                    'movie_id' => 3,
+                    'name' => 'Actor name 3',
+                    'age' => 20]);
+  }
+
+
+  /** @test */
+  public function withLeftJoinShouldReturn() {
+    $row_results = Storm_Test_LoaderQueryMovie::join('tab1')
+      ->select(['name'])
+      ->left(Storm_Test_LoaderQueryActor::left('tab2')
+             ->select(['name'])
+             ->on_eq('id', 'movie_id'))
+      ->fetchAll();
+
+    $this->assertCount(5, $row_results);
+    $this->assertEquals([['tab1_name' => 'Movie name 1',
+                          'tab2_name' => 'Actor name 2'],
+                         ['tab1_name' => 'Movie name 2',
+                          'tab2_name' => null],
+                         ['tab1_name' => 'Movie name 3',
+                          'tab2_name' => 'Actor name 1'],
+                         ['tab1_name' => 'Movie name 3',
+                          'tab2_name' => 'Actor name 3'],
+                         ['tab1_name' => 'Movie name 4',
+                          'tab2_name' => null]],
+                        array_map(fn($row) => $row->toArray(),
+                                  $row_results));
+    $this->assertTrue($row_results[0] instanceof Storm_Model_Row);
+
+    $this->assertSql('SELECT `tab1`.`name` AS `tab1_name`, `tab2`.`name` AS `tab2_name` FROM `movies` AS `tab1`
+ LEFT JOIN `actors` AS `tab2` ON `tab1`.`id` = `tab2`.`movie_id`');
+  }
+}
+
+
+
+
+class Storm_Test_LoaderQueryVolatileJoinRightTest
+  extends Storm_Test_ModelTestCase {
+
+  protected array $_storm_scopes = ['LoaderQueryVolatileJoinRightTest'];
+
+  public function setUp() {
+    parent::setUp();
+
+    $this->fixture(Storm_Test_LoaderQueryMovie::class,
+                   ['id' => 1,
+                    'name' => 'Movie name 1',
+                    'date' => '2020']);
+    $this->fixture(Storm_Test_LoaderQueryMovie::class,
+                   ['id' => 2,
+                    'name' => 'Movie name 2',
+                    'date' => '2022']);
+    $this->fixture(Storm_Test_LoaderQueryMovie::class,
+                   ['id' => 3,
+                    'name' => 'Movie name 3',
+                    'date' => '2021']);
+
+    $this->fixture(Storm_Test_LoaderQueryActor::class,
+                   ['id' => 1,
+                    'movie_id' => 4,
+                    'name' => 'Actor name 1',
+                    'age' => 25]);
+    $this->fixture(Storm_Test_LoaderQueryActor::class,
+                   ['id' => 2,
+                    'movie_id' => 1,
+                    'name' => 'Actor name 2',
+                    'age' => 52]);
+    $this->fixture(Storm_Test_LoaderQueryActor::class,
+                   ['id' => 3,
+                    'movie_id' => 1,
+                    'name' => 'Actor name 3',
+                    'age' => 20]);
+  }
+
+
+  /** @test */
+  public function withRightJoinShouldReturn() {
+    $row_results = Storm_Test_LoaderQueryMovie::join('tab1')
+      ->select(['name'])
+      ->right(Storm_Test_LoaderQueryActor::right('tab2')
+              ->select(['name'])
+              ->on_eq('id', 'movie_id'))
+      ->fetchAll();
+
+    $this->assertCount(3, $row_results);
+    $this->assertEquals([['tab1_name' => null,
+                          'tab2_name' => 'Actor name 1'],
+                         ['tab1_name' => 'Movie name 1',
+                          'tab2_name' => 'Actor name 2'],
+                         ['tab1_name' => 'Movie name 1',
+                          'tab2_name' => 'Actor name 3']],
+                        array_map(fn($row) => $row->toArray(),
+                                  $row_results));
+    $this->assertTrue($row_results[0] instanceof Storm_Model_Row);
+
+    $this->assertSql('SELECT `tab1`.`name` AS `tab1_name`, `tab2`.`name` AS `tab2_name` FROM `movies` AS `tab1`
+ RIGHT JOIN `actors` AS `tab2` ON `tab1`.`id` = `tab2`.`movie_id`');
+  }
+}
+
+
+
+
+class Storm_Test_LoaderQueryVolatileJoinMultipleTest
   extends Storm_Test_ModelTestCase {
 
+  protected array $_storm_scopes = ['LoaderQueryVolatileJoinMultipleTest'];
+
   public function setUp() {
     parent::setUp();
 
+    $this->fixture(Storm_Test_LoaderQueryMovie::class,
+                   ['id' => 1,
+                    'name' => 'Movie name 1',
+                    'date' => '2020']);
+    $this->fixture(Storm_Test_LoaderQueryMovie::class,
+                   ['id' => 2,
+                    'name' => 'Movie name 2',
+                    'date' => '2022']);
+    $this->fixture(Storm_Test_LoaderQueryMovie::class,
+                   ['id' => 3,
+                    'name' => 'Movie name 3',
+                    'date' => '2021']);
+    $this->fixture(Storm_Test_LoaderQueryMovie::class,
+                   ['id' => 4,
+                    'name' => 'Movie name 4',
+                    'date' => '2018']);
+    $this->fixture(Storm_Test_LoaderQueryMovie::class,
+                   ['id' => 5,
+                    'name' => 'Movie name 5',
+                    'date' => '2019']);
+
+    $this->fixture(Storm_Test_LoaderQueryActor::class,
+                   ['id' => 1,
+                    'movie_id' => 3,
+                    'name' => 'Actor name 1',
+                    'age' => 25]);
+    $this->fixture(Storm_Test_LoaderQueryActor::class,
+                   ['id' => 2,
+                    'movie_id' => 1,
+                    'name' => 'Actor name 2',
+                    'age' => 52]);
+    $this->fixture(Storm_Test_LoaderQueryActor::class,
+                   ['id' => 3,
+                    'movie_id' => 4,
+                    'name' => 'Actor name 3',
+                    'age' => 36]);
+    $this->fixture(Storm_Test_LoaderQueryActor::class,
+                   ['id' => 4,
+                    'movie_id' => 3,
+                    'name' => 'Actor name 4',
+                    'age' => 20]);
+
     $this->fixture(Storm_Test_LoaderQueryUser::class,
-                   ['id' => 100,
-                    'login' => 'user_admin',
-                    'level' => 'invite admin redacteur',
-                    'foo' => 'first second third',
-                    'order1' => 'abcd',
-                    'order2' => 2,
-                    'order3' => 'defg']);
+                   ['id' => 1,
+                    'libelle' => 'User 1',
+                    'movie_id' => 2]);
+    $this->fixture(Storm_Test_LoaderQueryUser::class,
+                   ['id' => 2,
+                    'libelle' => 'User 2',
+                    'movie_id' => 3]);
+    $this->fixture(Storm_Test_LoaderQueryUser::class,
+                   ['id' => 3,
+                    'libelle' => 'User 3',
+                    'movie_id' => 4]);
+
+    $this->fixture(Storm_Test_LoaderQueryProductor::class,
+                   ['id' => 1,
+                    'label' => 'Productor 1',
+                    'user_id' => 1,
+                    'birthday' => '01/01/2015']);
+    $this->fixture(Storm_Test_LoaderQueryProductor::class,
+                   ['id' => 2,
+                    'label' => 'Productor 2',
+                    'user_id' => 1,
+                    'birthday' => '02/02/2016']);
+    $this->fixture(Storm_Test_LoaderQueryProductor::class,
+                   ['id' => 3,
+                    'label' => 'Productor 3',
+                    'user_id' => 3,
+                    'birthday' => '03/03/2017']);
+    $this->fixture(Storm_Test_LoaderQueryProductor::class,
+                   ['id' => 4,
+                    'label' => 'Productor 4',
+                    'user_id' => 3,
+                    'birthday' => '04/04/2018']);
+  }
+
+
+  /** @test */
+  public function withInnerJoinShouldReturnIntersectMoviesActorsOnlyOneResult() {
+    $results = Storm_Test_LoaderQueryMovie::join('tab1')
+      ->select(['date'])
+      ->inner(Storm_Test_LoaderQueryActor::inner('tab2')
+              ->on_eq('id', 'movie_id')
+              ->select(['name'])
+              ->gt('age', 30))
+      ->inner(Storm_Test_LoaderQueryUser::inner('tab3')
+              ->on_eq('id', 'movie_id')
+              ->select(['libelle'])
+              ->right(Storm_Test_LoaderQueryProductor::right('tab4')
+                      ->on_eq('id', 'user_id')
+                      ->select(['label', 'birthday'])))
+      ->fetchAll();
+
+    $this->assertCount(4, $results);
+    $this->assertSql('SELECT `tab1`.`date` AS `tab1_date`, `tab2`.`name` AS `tab2_name`, `tab3`.`libelle` AS `tab3_libelle`, `tab4`.`label` AS `tab4_label`, `tab4`.`birthday` AS `tab4_birthday` FROM `movies` AS `tab1`
+ INNER JOIN `actors` AS `tab2` ON `tab1`.`id` = `tab2`.`movie_id` AND `tab2`.`age` > 30
+ INNER JOIN `users` AS `tab3` ON `tab1`.`id` = `tab3`.`movie_id`
+ RIGHT JOIN `productors` AS `tab4` ON `tab3`.`id` = `tab4`.`user_id`');
+    $this->assertEquals([['tab1_date' => null,
+                          'tab2_name' => null,
+                          'tab3_libelle' => null,
+                          'tab4_label' => 'Productor 1',
+                          'tab4_birthday' => '01/01/2015'],
+                         ['tab1_date' => null,
+                          'tab2_name' => null,
+                          'tab3_libelle' => null,
+                          'tab4_label' => 'Productor 2',
+                          'tab4_birthday' => '02/02/2016'],
+                         ['tab1_date' => '2018',
+                          'tab2_name' => 'Actor name 3',
+                          'tab3_libelle' => 'User 3',
+                          'tab4_label' => 'Productor 3',
+                          'tab4_birthday' => '03/03/2017'],
+                         ['tab1_date' => '2018',
+                          'tab2_name' => 'Actor name 3',
+                          'tab3_libelle' => 'User 3',
+                          'tab4_label' => 'Productor 4',
+                          'tab4_birthday' => '04/04/2018']],
+                        array_map(fn($row) => $row->toArray(),
+                                  $results));
+    $this->assertTrue($results[0] instanceof Storm_Model_Row);
+  }
+}
+
+
+
+
+class Storm_Test_LoaderQueryVolatileInSubQueriesTest
+  extends Storm_Test_ModelTestCase {
+
+  protected array $_storm_scopes = ['LoaderQueryVolatileInSubQueriesTest'];
+
+  public function setUp() {
+    parent::setUp();
 
     $this->fixture(Storm_Test_LoaderQueryUser::class,
-                   ['id' => 101,
-                    'login' => 'user_administrateur',
-                    'level' => 'administrateur',
-                    'foo' => 'third second forth',
-                    'order1' => 'abcdef',
-                    'order2' => 3,
-                    'order3' => 'defg']);
+                   ['id' => 1,
+                    'name' => 'User name 1',
+                    'year' => '2020',
+                    'user_id' => 1]);
+    $this->fixture(Storm_Test_LoaderQueryUser::class,
+                   ['id' => 2,
+                    'name' => 'User name 2',
+                    'year' => '2022',
+                    'user_id' => 2]);
+    $this->fixture(Storm_Test_LoaderQueryUser::class,
+                   ['id' => 3,
+                    'name' => 'User name 3',
+                    'year' => '2021',
+                    'user_id' => 3]);
+    $this->fixture(Storm_Test_LoaderQueryUser::class,
+                   ['id' => 4,
+                    'name' => 'User name 4',
+                    'year' => '2018',
+                    'user_id' => 4]);
+  }
+
+
+  /** @test */
+  public function withInSubQueriesReturnUser_Name3_Name4() {
+    $results = Storm_Test_LoaderQueryUser::query()
+      ->in('user_id', Storm_Test_LoaderQueryUser::subIn('users2')
+           ->select(['user_id'])
+           ->in('year', ['2021', '2018']))
+      ->fetchAll();
+
+    $this->assertCount(2, $results);
+    $this->assertEquals(['User name 3', 'User name 4'],
+                        array_map(fn($row) => $row->getName(),
+                                  $results));
+    $this->assertTrue($results[0] instanceof Storm_Test_LoaderQueryUser);
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE (`users`.`user_id` IN (SELECT `users2`.`user_id` FROM `users` AS `users2` WHERE (`users2`.`year` IN (\'2021\', \'2018\'))))');
+  }
+}
+
+
+
+
+class Storm_Test_LoaderQueryVolatileNotInSubQueriesTest
+  extends Storm_Test_ModelTestCase {
+
+  protected array $_storm_scopes = ['LoaderQueryVolatileNotInSubQueriesTest'];
+
+  public function setUp() {
+    parent::setUp();
 
     $this->fixture(Storm_Test_LoaderQueryUser::class,
-                   ['id' => 102,
-                    'login' => 'user_invite',
-                    'level' => 'invite',
-                    'foo' => 'forth fifth seven',
-                    'order1' => 'cdef',
-                    'order2' => 1,
-                    'order3' => 'klmn']);
+                   ['id' => 1,
+                    'name' => 'User name 1',
+                    'actor_id' => 1]);
+    $this->fixture(Storm_Test_LoaderQueryUser::class,
+                   ['id' => 2,
+                    'name' => 'User name 2',
+                    'actor_id' => 2]);
+    $this->fixture(Storm_Test_LoaderQueryUser::class,
+                   ['id' => 3,
+                    'name' => 'User name 3',
+                    'actor_id' => 3]);
+
+    $this->fixture(Storm_Test_LoaderQueryActor::class,
+                   ['id' => 2,
+                    'id_actor' => 2,
+                    'label' => 'Actor 2']);
+    $this->fixture(Storm_Test_LoaderQueryActor::class,
+                   ['id' => 3,
+                    'id_actor' => 3,
+                    'label' => 'Actor 3']);
   }
 
 
   /** @test */
-  public function with_Order1Desc_ShouldAnswersInOrdered() {
-    $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class)
-      ->order_desc('order1');
-    $this->assertEquals(['user_invite', 'user_administrateur', 'user_admin'],
-                        (new Storm_Model_Collection($query->fetchAll()))
-                        ->collect('login')
-                        ->getArrayCopy());
+  public function withNotInSubQueriesReturnUser_Name1_Name2() {
+    $results = Storm_Test_LoaderQueryUser::query()
+      ->not_in('actor_id', Storm_Test_LoaderQueryActor::subIn()
+               ->select(['id_actor'])
+               ->eq('label', 'Actor 3'))
+      ->fetchAll();
+
+    $this->assertCount(2, $results);
+    $this->assertEquals(['id' => 1,
+                         'name' => 'User name 1',
+                         'actor_id' => 1], $results[0]->toArray());
+    $this->assertEquals(['id' => 2,
+                         'name' => 'User name 2',
+                         'actor_id' => 2], $results[1]->toArray());
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE (`users`.`actor_id` NOT IN (SELECT `actors`.`id_actor` FROM `actors` WHERE (`actors`.`label` = \'Actor 3\')))');
+  }
+}
+
+
+
+
+class Storm_Test_LoaderQueryVolatileExistsSubQueriesTest
+  extends Storm_Test_ModelTestCase {
+
+  protected array $_storm_scopes = ['LoaderQueryVolatileExistsSubQueriesTest'];
+
+  public function setUp() {
+    parent::setUp();
+
+    $this->fixture(Storm_Test_LoaderQueryUser::class,
+                   ['id' => 1,
+                    'name' => 'User name 1',
+                    'actor_id' => 1,
+                    'age' => 21]);
+    $this->fixture(Storm_Test_LoaderQueryUser::class,
+                   ['id' => 2,
+                    'name' => 'User name 2',
+                    'actor_id' => 2,
+                    'age' => 22]);
+    $this->fixture(Storm_Test_LoaderQueryUser::class,
+                   ['id' => 3,
+                    'name' => 'User name 3',
+                    'actor_id' => 3,
+                    'age' => 23]);
+
+    $this->fixture(Storm_Test_LoaderQueryActor::class,
+                   ['id' => 2,
+                    'id_actor' => 2,
+                    'label' => 'Actor 2',
+                    'age' => 22]);
+    $this->fixture(Storm_Test_LoaderQueryActor::class,
+                   ['id' => 3,
+                    'id_actor' => 3,
+                    'label' => 'Actor 3',
+                    'age' => 20]);
+  }
+
+
+  /** @test */
+  public function withExistsSubQueriesReturnOnlyUser_Name3() {
+    $results = Storm_Test_LoaderQueryUser::query()
+      ->exists(Storm_Test_LoaderQueryActor::subExists()
+               ->on_eq('actor_id', 'id_actor')
+               ->on_gt('age', 'age'))
+      ->fetchAll();
+
+    $this->assertCount(1, $results);
+    $this->assertEquals(['id' => 3,
+                         'name' => 'User name 3',
+                         'actor_id' => 3,
+                         'age' => 23], $results[0]->toArray());
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE (EXISTS (SELECT \'x\' FROM `actors` WHERE (`users`.`actor_id` = `actors`.`id_actor` AND `users`.`age` > `actors`.`age`)))');
   }
 
 
   /** @test */
-  public function with_Order3Asc_Order2Desc_ShouldAnswersOrderedUser() {
-    $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class)
-      ->order('order3')
-      ->order_desc('order2');
-    $this->assertEquals(['user_administrateur', 'user_admin', 'user_invite'],
-                        (new Storm_Model_Collection($query->fetchAll()))
-                        ->collect('login')
-                        ->getArrayCopy());
+  public function withExistsSubQueriesReturnOnlyUser_Name2() {
+    $results = Storm_Test_LoaderQueryUser::query()
+      ->exists(Storm_Test_LoaderQueryActor::subExists()
+               ->on_gt_eq('actor_id', 'id_actor')
+               ->on_lt_eq('age', 'age'))
+      ->fetchAll();
+
+    $this->assertCount(1, $results);
+    $this->assertEquals(['id' => 2,
+                         'name' => 'User name 2',
+                         'actor_id' => 2,
+                         'age' => 22], $results[0]->toArray());
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE (EXISTS (SELECT \'x\' FROM `actors` WHERE (`users`.`actor_id` >= `actors`.`id_actor` AND `users`.`age` <= `actors`.`age`)))');
   }
 
 
   /** @test */
-  public function with_Order3Desc_Order2Desc_ShouldAnswersOrderedUser() {
-    $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class)
-      ->order_desc('order3')
-      ->order_desc('order2');
-    $this->assertEquals(['user_invite', 'user_administrateur', 'user_admin'],
-                        (new Storm_Model_Collection($query->fetchAll()))
-                        ->collect('login')
-                        ->getArrayCopy());
+  public function withNotExistsSubQueriesWithTwoConditionReturnUser_Name2_Name3() {
+    $results = Storm_Test_LoaderQueryUser::query()
+      ->not_exists(Storm_Test_LoaderQueryActor::subExists()
+                   ->on_not_eq('actor_id', 'id_actor')
+                   ->on_lt('age', 'age'))
+      ->fetchAll();
+
+    $this->assertCount(2, $results);
+    $this->assertEquals(['id' => 2,
+                         'name' => 'User name 2',
+                         'actor_id' => 2,
+                         'age' => 22], $results[0]->toArray());
+    $this->assertEquals(['id' => 3,
+                         'name' => 'User name 3',
+                         'actor_id' => 3,
+                         'age' => 23], $results[1]->toArray());
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE (NOT EXISTS (SELECT \'x\' FROM `actors` WHERE (`users`.`actor_id` != `actors`.`id_actor` AND `users`.`age` < `actors`.`age`)))');
   }
 
 
   /** @test */
-  public function withMatch_Level_Administrateur_ShouldAnswersOrderedUser() {
-    $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class)
-      ->order((new Storm_Query_MatchRating('level'))
-              ->against('administrateur'));
-    $this->assertEquals(['user_admin', 'user_invite', 'user_administrateur'],
-                        (new Storm_Model_Collection($query->fetchAll()))
-                        ->collect('login')
-                        ->getArrayCopy());
+  public function withNotExistsSubQueriesWithOneConditionAndOneCriteriaReturnUser_Name1_Name2() {
+    $results = Storm_Test_LoaderQueryUser::query()
+      ->not_exists(Storm_Test_LoaderQueryActor::subExists()
+                   ->on_eq('actor_id', 'id_actor')
+                   ->lt('age', 22))
+      ->fetchAll();
+
+    $this->assertCount(2, $results);
+    $this->assertEquals(['id' => 1,
+                         'name' => 'User name 1',
+                         'actor_id' => 1,
+                         'age' => 21], $results[0]->toArray());
+    $this->assertEquals(['id' => 2,
+                         'name' => 'User name 2',
+                         'actor_id' => 2,
+                         'age' => 22], $results[1]->toArray());
+    $this->assertSql('SELECT `users`.* FROM `users` WHERE (NOT EXISTS (SELECT \'x\' FROM `actors` WHERE (`users`.`actor_id` = `actors`.`id_actor` AND `actors`.`age` < 22)))');
+  }
+}
+
+
+
+
+class Storm_Test_LoaderQueryWithErrorsTest
+  extends Storm_Test_ModelTestCase {
+
+  protected array $_storm_scopes = ['Storm_Test_LoaderQueryWithErrorsTest'];
+
+  public function setUp() {
+    parent::setUp();
+
+    $this->fixture(Storm_Test_LoaderQueryUser::class,
+                   ['id' => 1,
+                    'name' => 'User name 1',
+                    'actor_id' => 1,
+                    'age' => 21]);
+
+    $this->fixture(Storm_Test_LoaderQueryActor::class,
+                   ['id' => 2,
+                    'id_actor' => 1,
+                    'label' => 'Actor 2',
+                    'age' => 22]);
   }
 
 
   /** @test */
-  public function withMatch_Level_Administrateur_Redacteur_Desc_ShouldAnswersOrderedUser() {
-    $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class)
-      ->order_desc((new Storm_Query_MatchRating('level'))
-                   ->against('administrateur redacteur'));
-    $this->assertEquals(['user_administrateur', 'user_admin', 'user_invite'],
-                        (new Storm_Model_Collection($query->fetchAll()))
-                        ->collect('login')
-                        ->getArrayCopy());
+  public function existsWithNoConditionAndSelectColumnReturnNothingAndSqlContainErrorMessage() {
+    $results = Storm_Test_LoaderQueryUser::query()
+      ->exists(Storm_Test_LoaderQueryActor::subExists()
+               ->select(['error']))
+      ->fetchAll();
+
+    $this->assertCount(0, $results);
+    $this->assertSql('Error: For Exists don\'t add select column, Error: For Exists condition "on" is mandatory');
   }
 
 
   /** @test */
-  public function withMatchInBooleanMode_Level_Administrateur_Redacteur_Desc_ShouldAnswersOrderedUser() {
-    $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class)
-      ->order_desc((new Storm_Query_MatchBoolean('level'))
-                   ->against('administrateur redacteur'));
-    $this->assertEquals(['user_admin', 'user_administrateur', 'user_invite'],
-                        (new Storm_Model_Collection($query->fetchAll()))
-                        ->collect('login')
-                        ->getArrayCopy());
+  public function inWithNoSelectColumnReturnNothingAndSqlContainErrorMessage() {
+    $results = Storm_Test_LoaderQueryUser::query()
+      ->in('actor_id', Storm_Test_LoaderQueryActor::subIn())
+      ->fetchAll();
+
+    $this->assertCount(0, $results);
+    $this->assertSql('Error: For In add select column is mandatory');
   }
 
 
   /** @test */
-  public function withMatch_FooDescExacteInBooleanMode_Forth_Fifth_Level_Administrateur_Redacteur_ShouldAnswersOrderedUser() {
-    $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class)
-      ->order_desc((new Storm_Query_MatchBoolean('foo'))
-                   ->exact('forth fifth'))
-      ->order((new Storm_Query_MatchRating('level'))
-              ->against('administrateur redacteur'));
-    $this->assertEquals(['user_invite', 'user_admin', 'user_administrateur'],
-                        (new Storm_Model_Collection($query->fetchAll()))
-                        ->collect('login')
-                        ->getArrayCopy());
+  public function inWithMultipleSelectColumnReturnNothingAndSqlContainErrorMessage() {
+    $results = Storm_Test_LoaderQueryUser::query()
+      ->in('actor_id', Storm_Test_LoaderQueryActor::subIn()
+           ->select(['id', 'id_actor']))
+      ->fetchAll();
+
+    $this->assertCount(0, $results);
+    $this->assertSql('Error: For In add select must have only one column');
   }
 
 
   /** @test */
-  public function withMatch_FooLevelStrict_InviteRedacteur_SecondThird_ShouldAnswersOrderedUser() {
-    // https://www.php.net/manual/fr/function.usort
-    // If two members compare as equal, they retain their original order. Prior to PHP 8.0.0, their relative order in the sorted array was undefined.
-    if (version_compare(PHP_VERSION, '8.0.0', '<'))
-      return;
+  public function innerWithNoConditionReturnNothingAndSqlContainErrorMessage() {
+    $results = Storm_Test_LoaderQueryUser::join('tab1')
+      ->inner(Storm_Test_LoaderQueryActor::inner('tab2'))
+      ->fetchAll();
 
-    $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class)
-      ->order((new Storm_Query_MatchBoolean('foo,level', true))
-              ->against('invite redacteur')
-              ->against('second third'));
-    $this->assertEquals([Storm_Test_LoaderQueryUser::find(101),
-                         Storm_Test_LoaderQueryUser::find(102),
-                         Storm_Test_LoaderQueryUser::find(100)],
-                        $query->fetchAll());
+    $this->assertCount(0, $results);
+    $this->assertSql('Error: For Inner condition "on" is mandatory');
   }
 
 
   /** @test */
-  public function selectUsersGroupByOrder3ShouldAnswersUserAdminAndUserInvite() {
-    $query = Storm_Query::from(Storm_Test_LoaderQueryUser::class)
-      ->group('order3');
-    $this->assertEquals([0 => 'user_admin', 2 => 'user_invite'],
-                        (new Storm_Model_Collection($query->fetchAll()))
-                        ->collect('login')
-                        ->getArrayCopy());
+  public function leftWithNoConditionReturnNothingAndSqlContainErrorMessage() {
+    $results = Storm_Test_LoaderQueryUser::join('tab1')
+      ->left(Storm_Test_LoaderQueryActor::left('tab2'))
+      ->fetchAll();
+
+    $this->assertCount(0, $results);
+    $this->assertSql('Error: For Left condition "on" is mandatory');
   }
+
+
+  /** @test */
+  public function rightWithNoConditionReturnNothingAndSqlContainErrorMessage() {
+    $results = Storm_Test_LoaderQueryUser::join('tab1')
+      ->right(Storm_Test_LoaderQueryActor::right('tab2'))
+      ->fetchAll();
+
+    $this->assertCount(0, $results);
+    $this->assertSql('Error: For Right condition "on" is mandatory');
+  }
+}
+
+
+
+
+class Storm_Test_LoaderQueryUser extends Storm_Model_Abstract {
+
+  protected $_table_name = 'users';
+}
+
+
+
+
+class Storm_Test_LoaderQueryMovie extends Storm_Model_Abstract {
+
+  protected $_table_name = 'movies';
+}
+
+
+
+
+class Storm_Test_LoaderQueryActor extends Storm_Model_Abstract {
+
+  protected $_table_name = 'actors';
+}
+
+
+
+
+class Storm_Test_LoaderQueryProductor extends Storm_Model_Abstract {
+
+  protected $_table_name = 'productors';
 }
diff --git a/tests/Storm/Test/LoaderTest.php b/tests/Storm/Test/LoaderTest.php
index 50eec16ee9072b6432b7526be00df3e1d968765c..0ffa2029154771ccf42a2e489e607f58586ca48e 100644
--- a/tests/Storm/Test/LoaderTest.php
+++ b/tests/Storm/Test/LoaderTest.php
@@ -25,12 +25,14 @@
 
 
 abstract class Storm_Test_LoaderTestCase extends Storm_Test_ModelTestCase {
+
   protected
     $_loader,
     $_table;
 
   public function setUp() {
     parent::setUp();
+
     $this->_table = $this->mock();
     $this->_table
       ->whenCalled('getAdapter')
@@ -112,7 +114,7 @@ class Storm_Test_LoaderBasicTest extends Storm_Test_LoaderTestCase {
   public function withNullValueFindAllByShouldBuildSqlWithIsNullStatement() {
     $select = $this->mock()
                    ->whenCalled('where')
-                   ->with('parent_id is null')
+                   ->with('`parent_id` IS null')
                    ->answers(null)
 
                    ->whenCalled('where')
@@ -157,18 +159,19 @@ class Storm_Test_LoaderBasicTest extends Storm_Test_LoaderTestCase {
   public function findAllByGeneratedSelects() {
     return
       [
-       [ ['name' => null] , ['name is null'] ],
-       [ ['name' => ['Harlock', 'Nausicaa']], ['name in (\'Harlock\', \'Nausicaa\')'] ],
-       [ ['name not' => 'Harlock'], ['name!=\'Harlock\''] ],
+       [ ['name' => null] , ['`name` IS null'] ],
+       [ ['name' => ['Harlock', 'Nausicaa']], ['`name` IN (\'Harlock\', \'Nausicaa\')'] ],
+       [ ['name not' => 'Harlock'], ['`name` != \'Harlock\''] ],
        [ ['name not' => ['Harlock', 'Nausicaa']],
-        ['name not in (\'Harlock\', \'Nausicaa\')'] ],
-       [ ['name not' => null], ['name is not null'] ],
-       [ ['login like' => '%aus%'], ['login like \'%aus%\''] ],
-       [ ['login not like' => '%aus%'], ['login not like \'%aus%\''] ],
+        ['`name` NOT IN (\'Harlock\', \'Nausicaa\')'] ],
+       [ ['name not' => null], ['`name` IS NOT null'] ],
+       [ ['login like' => '%aus%'], ['`login` LIKE \'%aus%\''] ],
+       [ ['login not like' => '%aus%'], ['`login` NOT LIKE \'%aus%\''] ],
        [ ['where' => 'id=\'29\' and name=\'lp\''], ['id=\'29\' and name=\'lp\''] ],
-       [ ['left(name, 5)' => 'ganda'], ['left(name, 5)=\'ganda\''] ],
-       [ ['id' => '29'], ['id=\'29\''] ],
-       [ [ Storm_Query_Clause::greater('id', '30') ], ['id>\'30\''] ],
+       [ ['left(name, 5)' => 'ganda'], ['LEFT(`name`, 5) = \'ganda\''] ],
+       [ ['id' => '29'], ['`id` = \'29\''] ],
+       [ [ Storm_Query_Clause::greater('id', '30') ], ['`id` > \'30\''] ],
+       [ [ Storm_Query_Clause::left('name', 5, 'ganda') ], ['LEFT(`name`, 5) = \'ganda\''] ],
       ];
   }
 
@@ -209,7 +212,7 @@ class Storm_Test_LoaderBasicTest extends Storm_Test_LoaderTestCase {
     $this->_table->whenCalled('delete')->answers(10);
     $this->_loader->basicDeleteBy(['nom' => 'bond', 'prenom' => 'james']);
 
-    $this->assertEquals('nom=\'bond\' and prenom=\'james\'',
+    $this->assertEquals('`nom` = \'bond\' and `prenom` = \'james\'',
                         $this->_table->getFirstAttributeForLastCallOn('delete'));
   }
 
@@ -219,7 +222,7 @@ class Storm_Test_LoaderBasicTest extends Storm_Test_LoaderTestCase {
     $this->_table->whenCalled('delete')->answers(2);
     $this->_loader->basicDeleteBy(['nom' => ['bond', 'l\'upin']]);
 
-    $this->assertEquals("nom in ('bond', 'l'upin')",
+    $this->assertEquals("`nom` IN ('bond', 'l'upin')",
                         $this->_table->getFirstAttributeForLastCallOn('delete'));
   }
 
@@ -240,7 +243,7 @@ class Storm_Test_LoaderBasicTest extends Storm_Test_LoaderTestCase {
     $this->_loader->basicDeleteBy(['where' => 'nom > "22"',
                                    'nom' => 'bond']);
 
-    $this->assertEquals('(nom > "22") and nom=\'bond\'',
+    $this->assertEquals('(nom > "22") and `nom` = \'bond\'',
                         $this->_table->getFirstAttributeForLastCallOn('delete'));
   }
 
@@ -262,7 +265,7 @@ class Storm_Test_LoaderBasicTest extends Storm_Test_LoaderTestCase {
                               ['type' => 'fictive']);
 
     $this->assertEquals([['type' => 'fictive'],
-                         "nom in ('bond', 'lupin') and type='real'"],
+                         "`nom` IN ('bond', 'lupin') and `type` = 'real'"],
                         $this->_table->getAttributesForLastCallOn('update'));
   }
 
@@ -274,7 +277,7 @@ class Storm_Test_LoaderBasicTest extends Storm_Test_LoaderTestCase {
                               ['type' => 'fictive']);
 
     $this->assertEquals([['type' => 'fictive'],
-                         "nom!='bond'"],
+                         "`nom` != 'bond'"],
                         $this->_table->getAttributesForLastCallOn('update'));
   }
 
@@ -293,7 +296,7 @@ class Storm_Test_LoaderSaveWithIdTest extends Storm_Test_LoaderTestCase {
       ->answers($this->_select = $this->mock());
 
     $this->_select
-      ->whenCalled('where')->with('id=4')->answers($this->_select)
+      ->whenCalled('where')->with('`id` = 4')->answers($this->_select)
       ->whenCalled('from')->with($this->_table, ['count(*) as numberof'])->answers($this->_select);
 
   }