diff --git a/src/Storm/Model/Loader.php b/src/Storm/Model/Loader.php
index 0d7ba1ddf118726444a5cef3015dd232dacb3020..ead8016d25774d4a78aa94fee64fe492e569cfe9 100644
--- a/src/Storm/Model/Loader.php
+++ b/src/Storm/Model/Loader.php
@@ -90,7 +90,6 @@ class Storm_Model_Loader {
   }
 
 
-
   public static function resetCache() {
     static::$_loader_cache = null;
   }
@@ -474,77 +473,4 @@ class Storm_Model_Loader {
   public function fetchAllBy($fields, $params) {
     return $this->getPersistenceStrategy()->fetchAllBy($fields, $params);
   }
-
-
-  public function clauseLike(string $key, string $value) : Storm_Model_PersistenceStrategy_Clause {
-    return Storm_Model_PersistenceStrategy_Clause::clauseLike($key, $value);
-  }
-
-
-  public function clauseNotLike(string $key, string $value) : Storm_Model_PersistenceStrategy_Clause {
-    return Storm_Model_PersistenceStrategy_Clause::clauseNotLike($key, $value);
-  }
-
-
-  public function clauseGreater(string $key, string $value) : Storm_Model_PersistenceStrategy_Clause {
-    return Storm_Model_PersistenceStrategy_Clause::clauseGreater($key, $value);
-  }
-
-
-  public function clauseGreaterEqual(string $key, string $value) : Storm_Model_PersistenceStrategy_Clause {
-    return Storm_Model_PersistenceStrategy_Clause::clauseGreaterEqual($key, $value);
-  }
-
-
-  public function clauseLesser(string $key, string $value) : Storm_Model_PersistenceStrategy_Clause {
-    return Storm_Model_PersistenceStrategy_Clause::clauseLesser($key, $value);
-  }
-
-
-  public function clauseLesserEqual(string $key, string $value) : Storm_Model_PersistenceStrategy_Clause {
-    return Storm_Model_PersistenceStrategy_Clause::clauseLesserEqual($key, $value);
-  }
-
-
-  public function clauseIs(string $key) : Storm_Model_PersistenceStrategy_Clause {
-    return Storm_Model_PersistenceStrategy_Clause::clauseIs($key);
-  }
-
-
-  public function clauseNotIs(string $key) : Storm_Model_PersistenceStrategy_Clause {
-    return Storm_Model_PersistenceStrategy_Clause::clauseNotIs($key);
-  }
-
-
-  public function clauseIn(string $key, array $array) : Storm_Model_PersistenceStrategy_Clause {
-    return Storm_Model_PersistenceStrategy_Clause::clauseIn($key, $array);
-  }
-
-
-  public function clauseNotIn(string $key, array $array) : Storm_Model_PersistenceStrategy_Clause {
-    return Storm_Model_PersistenceStrategy_Clause::clauseNotIn($key, $array);
-  }
-
-
-  public function clauseEqual(string $key,
-                              string $value) : Storm_Model_PersistenceStrategy_Clause {
-    return Storm_Model_PersistenceStrategy_Clause::clauseEqual($key, $value);
-  }
-
-
-  public function clauseNotEqual(string $key, string $value) : Storm_Model_PersistenceStrategy_Clause {
-    return Storm_Model_PersistenceStrategy_Clause::clauseNotEqual($key, $value);
-  }
-
-
-  public function clauseStart(string $key,
-                              string $value) : Storm_Model_PersistenceStrategy_Clause {
-    return Storm_Model_PersistenceStrategy_Clause::clauseStart($key, $value);
-  }
-
-
-  public function clauseEnd(string $key,
-                            string $value) : Storm_Model_PersistenceStrategy_Clause {
-    return Storm_Model_PersistenceStrategy_Clause::clauseEnd($key, $value);
-  }
 }
diff --git a/src/Storm/Model/PersistenceStrategy/Clause.php b/src/Storm/Model/PersistenceStrategy/Clause.php
index b1db63ec7e60f8cde186de2f3f47de72bfce3f7d..a1c6025eb80253716197bb7caf12815a8cc4ced0 100644
--- a/src/Storm/Model/PersistenceStrategy/Clause.php
+++ b/src/Storm/Model/PersistenceStrategy/Clause.php
@@ -24,17 +24,17 @@ THE SOFTWARE.
 
 */
 
+
 class Storm_Model_PersistenceStrategy_Clause {
 
-  protected
-    $_key,
-    $_operator,
-    $_value,
-    $_array,
-    $_negated,
-    $_is_array;
+  protected string $_key;
+  protected string $_operator;
+  protected bool $_negated;
+  protected $_value_or_array;
 
   const
+    CLAUSE_OR = 'or',
+    CLAUSE_AND = 'and',
     CLAUSE_WHERE = 'where',
     CLAUSE_LIKE = 'like',
     CLAUSE_EQUAL = '=',
@@ -44,6 +44,10 @@ class Storm_Model_PersistenceStrategy_Clause {
     CLAUSE_GREATER_EQUAL = '>=',
     CLAUSE_LESSER = '<',
     CLAUSE_LESSER_EQUAL = '<=',
+    CLAUSE_MATCH = 'MATCH',
+    CLAUSE_LIMIT = 'limit',
+    CLAUSE_LIMIT_PAGE = 'limitPage',
+    CLAUSE_ORDER_BY = 'order by',
     PERCENT = '%';
 
 
@@ -51,184 +55,558 @@ class Storm_Model_PersistenceStrategy_Clause {
     $this->_negated = false;
     $this->_key = $key;
     $this->_operator = $operator;
-    $this->_is_array = is_array($value_or_array);
+    $this->_value_or_array = $value_or_array;
+  }
 
-    if ($this->_is_array)
-      $this->_array = $value_or_array;
 
-    if (!$this->_is_array)
-      $this->_value = $value_or_array;
+  public static function newFor(string $key, string $operator, $value_or_array) : self {
+    if (static::CLAUSE_OR === $operator)
+      return (new Storm_Model_PersistenceStrategy_ClauseOr($key, $operator, $value_or_array));
+    if (static::CLAUSE_AND === $operator)
+      return (new Storm_Model_PersistenceStrategy_ClauseAnd($key, $operator, $value_or_array));
+    if (static::CLAUSE_LIKE === $operator)
+      return (new Storm_Model_PersistenceStrategy_ClauseLike($key, $operator, $value_or_array));
+    if (static::CLAUSE_EQUAL === $operator)
+      return (new Storm_Model_PersistenceStrategy_ClauseEqual($key, $operator, $value_or_array));
+    if (static::CLAUSE_IN === $operator)
+      return (new Storm_Model_PersistenceStrategy_ClauseIn($key, $operator, $value_or_array));
+    if (static::CLAUSE_IS === $operator)
+      return (new Storm_Model_PersistenceStrategy_ClauseIs($key, $operator, $value_or_array));
+    if (static::CLAUSE_GREATER === $operator)
+      return (new Storm_Model_PersistenceStrategy_ClauseGreater($key, $operator, $value_or_array));
+    if (static::CLAUSE_GREATER_EQUAL === $operator)
+      return (new Storm_Model_PersistenceStrategy_ClauseGreaterEqual($key, $operator, $value_or_array));
+    if (static::CLAUSE_LESSER === $operator)
+      return (new Storm_Model_PersistenceStrategy_ClauseLesser($key, $operator, $value_or_array));
+    if (static::CLAUSE_LESSER_EQUAL === $operator)
+      return (new Storm_Model_PersistenceStrategy_ClauseLesserEqual($key, $operator, $value_or_array));
+    if (static::CLAUSE_MATCH === $operator)
+      return (new Storm_Model_PersistenceStrategy_ClauseMatch($key, $operator, $value_or_array));
+    if (static::CLAUSE_LIMIT === $operator)
+      return (new Storm_Model_PersistenceStrategy_ClauseLimit($key, $operator, $value_or_array));
+    if (static::CLAUSE_LIMIT_PAGE === $operator)
+      return (new Storm_Model_PersistenceStrategy_ClauseLimitPage($key, $operator, $value_or_array));
+    if (static::CLAUSE_ORDER_BY === $operator)
+      return (new Storm_Model_PersistenceStrategy_ClauseOrderBy($key, $operator, $value_or_array));
+
+    return (new static($key, $operator, $value_or_array));
   }
 
 
-  public static function clauseIs(string $key) : self {
-    return new static($key, static::CLAUSE_IS, null);
-  }
+  public static function newWith(string $key, $value_or_array) : self {
+    $operator = static::CLAUSE_EQUAL;
+    if (static::CLAUSE_WHERE === $key)
+      $operator = static::CLAUSE_WHERE;
+
+    $is_like = ' like' === substr($key, -5);
+    $key = $is_like ? substr($key, 0, strlen($key) - 5) : $key;
+    $negated = (' not' === substr($key, -4));
+    $key = $negated ? substr($key, 0, strlen($key) - 4) : $key;
+
+    if ($is_like)
+      $operator = static::CLAUSE_LIKE;
+
+    if (null === $value_or_array)
+      $operator = static::CLAUSE_IS;
 
+    if (is_array($value_or_array))
+      $operator = static::CLAUSE_IN;
 
-  public static function clauseNotIs(string $key) : self {
-    return static::clauseIs($key)->_setNegated(true);
+    return (static::newFor($key, $operator, $value_or_array))->setNegated($negated);
   }
 
 
-  public static function clauseIn(string $key, array $array) : self {
-    return new static($key, static::CLAUSE_IN, $array);
+  public static function is(string $key) : self {
+    return static::newFor($key, static::CLAUSE_IS, null);
   }
 
 
-  public static function clauseNotIn(string $key, array $array) : self {
-    return static::clauseIn($key, $array)->_setNegated(true);
+  public static function in(string $key, array $array) : self {
+    return static::newFor($key, static::CLAUSE_IN, $array);
   }
 
 
-  public static function clauseEqual(string $key, string $value) : self {
-    return new static($key, static::CLAUSE_EQUAL, $value);
+  public static function equal(string $key, string $value) : self {
+    return static::newFor($key, static::CLAUSE_EQUAL, $value);
   }
 
 
-  public static function clauseNotEqual(string $key, string $value) : self {
-    return static::clauseEqual($key, $value)->_setNegated(true);
+  public static function like(string $key, string $value) : self {
+    return static::newFor($key, static::CLAUSE_LIKE, $value);
   }
 
 
-  public static function clauseLike(string $key, string $value) : self {
-    return new static($key, static::CLAUSE_LIKE, $value);
+  public static function start(string $key, string $value) : self {
+    return static::newFor($key, static::CLAUSE_LIKE, $value . static::PERCENT);
   }
 
 
-  public static function clauseNotLike(string $key, string $value) : self {
-    return static::clauseLike($key, $value)->_setNegated(true);
+  public static function end(string $key, string $value) : self {
+    return static::newFor($key, static::CLAUSE_LIKE, static::PERCENT . $value);
   }
 
 
-  public static function clauseStart(string $key, string $value) : self {
-    return new static($key, static::CLAUSE_LIKE, $value . static::PERCENT);
+  public static function greater(string $key, string $value) : self {
+    return static::newFor($key, static::CLAUSE_GREATER, $value);
   }
 
 
-  public static function clauseEnd(string $key, string $value) : self {
-    return new static($key, static::CLAUSE_LIKE, static::PERCENT . $value);
+  public static function greaterEqual(string $key, string $value) : self {
+    return static::newFor($key, static::CLAUSE_GREATER_EQUAL, $value);
   }
 
 
-  public static function clauseGreater(string $key, string $value) : self {
-    return new static($key, static::CLAUSE_GREATER, $value);
+  public static function lesser(string $key, string $value) : self {
+    return static::newFor($key, static::CLAUSE_LESSER, $value);
   }
 
 
-  public static function clauseGreaterEqual(string $key, string $value) : self {
-    return new static($key, static::CLAUSE_GREATER_EQUAL, $value);
+  public static function lesserEqual(string $key, string $value) : self {
+    return static::newFor($key, static::CLAUSE_LESSER_EQUAL, $value);
   }
 
 
-  public static function clauseLesser(string $key, string $value) : self {
-    return new static($key, static::CLAUSE_LESSER, $value);
+  public static function or(array $array) : self {
+    return static::newFor(static::CLAUSE_OR, static::CLAUSE_OR, $array);
   }
 
 
-  public static function clauseLesserEqual(string $key, string $value) : self {
-    return new static($key, static::CLAUSE_LESSER_EQUAL, $value);
+  public static function and(array $array) : self {
+    return static::newFor(static::CLAUSE_AND, static::CLAUSE_AND, $array);
   }
 
 
-  public static function newWith(string $key, $value_or_array) : self {
-    $operator = static::CLAUSE_EQUAL;
-    if (static::CLAUSE_WHERE === $key)
-      $operator = static::CLAUSE_WHERE;
+  public static function match(string $key,
+                               array $array,
+                               ?bool $strict = false,
+                               ?bool $boolean_mode = true) : self {
+    return static::newFor($key,
+                          static::CLAUSE_MATCH,
+                          (new Storm_Model_PersistenceStrategy_Match($array, $strict, $boolean_mode)));
+  }
 
-    $is_like = ' like' === substr($key, -5);
-    $key = $is_like ? substr($key, 0, strlen($key) - 5) : $key;
-    $negated = (' not' === substr($key, -4));
-    $key = $negated ? substr($key, 0, strlen($key) - 4) : $key;
 
-    if ($is_like)
-      $operator = static::CLAUSE_LIKE;
+  public static function limit($limit) : self {
+    return static::newFor(static::CLAUSE_LIMIT, static::CLAUSE_LIMIT, $limit);
+  }
 
-    if (null === $value_or_array)
-      $operator = static::CLAUSE_IS;
 
-    if (is_array($value_or_array))
-      $operator = static::CLAUSE_IN;
+  public static function limit_page(array $range) : self {
+    return static::newFor(static::CLAUSE_LIMIT_PAGE, static::CLAUSE_LIMIT_PAGE, $range);
+  }
+
 
-    return (new static($key, $operator, $value_or_array))->_setNegated($negated);
+  public static function order(string $key, ?Storm_Model_PersistenceStrategy_Clause $clause = null) : self {
+    return static::newFor($key, static::CLAUSE_ORDER_BY, $clause);
   }
 
 
-  protected function _setNegated(bool $negated) : self {
+  public function setNegated(bool $negated) : self {
     $this->_negated = $negated;
     return $this;
   }
 
 
-  protected function _getValueOrArray() {
-    return $this->_is_array
-      ? ($this->_array ?? [])
-      : ($this->_value ?? '');
+  public function getValueOrArray() {
+    return $this->_value_or_array ?? '';
+  }
+
+
+  public function assemble($select) : self {
+    $select->where($this->getFormatDb($select->getTable()));
+    return $this;
   }
 
 
   public function getFormatDb($table) : string {
     if (static::CLAUSE_WHERE === $this->_operator)
-      return '(' . $this->_getValueOrArray() . ')';
-    if (static::CLAUSE_IS === $this->_operator)
-      return $this->_key . ' ' . $this->_getOperator() . ' null';
+      return '(' . $this->getValueOrArray() . ')';
 
     return $table->getAdapter()
                  ->quoteInto($this->_clauseFormatDb(),
-                             $this->_getValueOrArray(), null, null);
+                             $this->getValueOrArray(), null, null);
   }
 
 
-  protected function _clauseFormatDb() : string {
-    if (static::CLAUSE_IN === $this->_operator)
-      return $this->_key . ' ' . $this->_getOperator() . ' (?)';
-    if (static::CLAUSE_LIKE === $this->_operator)
-      return $this->_key . ' ' . $this->_getOperator() . ' ?';
+  public function containAttibuteInVolatile(array $model) : bool {
+    if (!$this->_existKeyInModel($model))
+      return $this->_negated;
 
+    return ($this->_negated !== ($model[$this->_key] == $this->getValueOrArray()));
+  }
+
+
+  protected function _clauseFormatDb() : string {
     return $this->_key . $this->_getOperator() . '?';
   }
 
 
   protected function _getOperator() : string {
-    if (static::CLAUSE_EQUAL === $this->_operator)
-      return ($this->_negated ? '!' : '') . $this->_operator;
-    if (static::CLAUSE_IS === $this->_operator)
-      return $this->_operator . ($this->_negated ? ' not' : '');
-
     return ($this->_negated ? 'not ' : '') . $this->_operator;
   }
 
 
-  public function containAttibuteInVolatile($model) : bool {
-    if (!array_key_exists($this->_key, $model))
+  protected function _existKeyInModel(array $model) : bool {
+    return array_key_exists($this->_key, $model);
+  }
+}
+
+
+
+
+class Storm_Model_PersistenceStrategy_ClauseOr
+  extends Storm_Model_PersistenceStrategy_Clause {
+
+  public function getValueOrArray() {
+    return $this->_value_or_array ?? [];
+  }
+
+
+  public function getFormatDb($table) : string {
+    $request = '';
+    $operator = ' ' . $this->_operator . ' ';
+    foreach ($this->getValueOrArray() as $clause)
+      $request = implode($operator, array_filter([$request,
+                                                  $clause->getFormatDb($table)]));
+
+    return '(' . $request . ')';
+  }
+
+
+  public function containAttibuteInVolatile(array $model) : bool {
+    foreach ($this->getValueOrArray() as $clause)
+      if ($clause->containAttibuteInVolatile($model))
+        return true;
+
+    return false;
+  }
+}
+
+
+
+
+class Storm_Model_PersistenceStrategy_ClauseAnd
+  extends Storm_Model_PersistenceStrategy_ClauseOr {
+
+  public function containAttibuteInVolatile(array $model) : bool {
+    foreach ($this->getValueOrArray() as $clause)
+      if (!$clause->containAttibuteInVolatile($model))
+        return false;
+
+    return true;
+  }
+}
+
+
+
+
+class Storm_Model_PersistenceStrategy_ClauseLike
+  extends Storm_Model_PersistenceStrategy_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('%', '.*',
+                                              $this->getValueOrArray()) . '$/i'),
+                          $model[$this->_key]);
+
+    return $this->_negated ? !$matches : $matches;
+  }
+}
+
+
+
+
+class Storm_Model_PersistenceStrategy_ClauseEqual
+  extends Storm_Model_PersistenceStrategy_Clause {
+
+  protected function _getOperator() : string {
+    return ($this->_negated ? '!' : '') . $this->_operator;
+  }
+}
+
+
+
+
+class Storm_Model_PersistenceStrategy_ClauseIn
+  extends Storm_Model_PersistenceStrategy_Clause {
+
+  public function getValueOrArray() {
+    return $this->_value_or_array ?? [];
+  }
+
+
+  public function containAttibuteInVolatile(array $model) : bool {
+    if (!$this->_existKeyInModel($model))
       return $this->_negated;
 
-    if ($this->_is_array && 0 === count($this->_getValueOrArray()))
+    if (0 === count($this->getValueOrArray()))
       throw new Storm_Model_Exception(sprintf('array given for %s is empty',
                                               $this->_key));
 
-    if ($this->_is_array && in_array($model[$this->_key], $this->_getValueOrArray()))
-      return !$this->_negated;
+    return in_array($model[$this->_key], $this->getValueOrArray())
+      ? !$this->_negated
+      : false;
+  }
+
+
+  protected function _clauseFormatDb() : string {
+    return $this->_key . ' ' . $this->_getOperator() . ' (?)';
+  }
+}
+
+
+
+
+class Storm_Model_PersistenceStrategy_ClauseIs
+  extends Storm_Model_PersistenceStrategy_Clause {
+
+  public function getValueOrArray() {
+    return null;
+  }
+
+
+  public function getFormatDb($table) : string {
+    return $this->_key . ' ' . $this->_getOperator() . ' null';
+  }
+
+
+  protected function _getOperator() : string {
+    return $this->_operator . ($this->_negated ? ' not' : '');
+  }
+}
+
+
+
+
+class Storm_Model_PersistenceStrategy_ClauseGreater
+  extends Storm_Model_PersistenceStrategy_Clause {
+
+  public function containAttibuteInVolatile(array $model) : bool {
+    if (!$this->_existKeyInModel($model))
+      return false;
+
+    return $model[$this->_key] > $this->getValueOrArray();
+  }
+}
+
+
+
+
+class Storm_Model_PersistenceStrategy_ClauseGreaterEqual
+  extends Storm_Model_PersistenceStrategy_Clause {
+
+  public function containAttibuteInVolatile(array $model) : bool {
+    if (!$this->_existKeyInModel($model))
+      return false;
+
+    return $model[$this->_key] >= $this->getValueOrArray();
+  }
+}
+
+
+
+
+class Storm_Model_PersistenceStrategy_ClauseLesser
+  extends Storm_Model_PersistenceStrategy_Clause {
+
+  public function containAttibuteInVolatile(array $model) : bool {
+    if (!$this->_existKeyInModel($model))
+      return false;
+
+    return $model[$this->_key] < $this->getValueOrArray();
+  }
+}
+
+
+
 
-    if (static::CLAUSE_LIKE === $this->_operator) {
-      $matches = preg_match(('/^' . str_replace('%', '.*',
-                                                $this->_getValueOrArray()) . '$/i'),
-                            $model[$this->_key]);
-      return $this->_negated ? !$matches : $matches;
-    }
+class Storm_Model_PersistenceStrategy_ClauseLesserEqual
+  extends Storm_Model_PersistenceStrategy_Clause {
+
+  public function containAttibuteInVolatile(array $model) : bool {
+    if (!$this->_existKeyInModel($model))
+      return false;
+
+    return $model[$this->_key] <= $this->getValueOrArray();
+  }
+}
+
+
+
+
+class Storm_Model_PersistenceStrategy_ClauseMatch
+  extends Storm_Model_PersistenceStrategy_Clause {
+
+  public function getValueOrArray() {
+    return $this->_value_or_array ?? null;
+  }
+
+
+  public function getFormatDb($table) : string {
+    return ($value = $this->getValueOrArray())
+      ? ($this->_getOperator() . ' ' . $value->getFormatDb($table))
+      : '';
+  }
+
+
+  public function compare(array $a, array $b) : int {
+    if (!($value = $this->getValueOrArray()))
+      return 0;
+
+    return ($value->getCompareValue($this->_getContents($a))
+            - $value->getCompareValue($this->_getContents($b)));
+  }
+
+
+  public function containAttibuteInVolatile(array $model) : bool {
+    if (!($value = $this->getValueOrArray()))
+      return false;
+
+    return $value->containVolatile($this->_getContents($model));
+  }
+
+
+  protected function _getOperator() : string {
+    return $this->_operator . '(' . $this->_key . ')';
+  }
+
+
+  protected function _clauseFormatDb() : string {
+    return $this->_getOperator();
+  }
+
+
+  protected function _getContents(array $model) : string {
+    $contents = '';
+    foreach (explode(',', $this->_key) as $key)
+      if (array_key_exists($key, $model))
+        $contents = implode(' ', array_filter([$contents, $model[$key]]));
+
+    return $contents;
+  }
+}
 
-    if (static::CLAUSE_GREATER === $this->_operator)
-      return $model[$this->_key] > $this->_getValueOrArray();
 
-    if (static::CLAUSE_GREATER_EQUAL === $this->_operator)
-      return $model[$this->_key] >= $this->_getValueOrArray();
 
-    if (static::CLAUSE_LESSER === $this->_operator)
-      return $model[$this->_key] < $this->_getValueOrArray();
 
-    if (static::CLAUSE_LESSER_EQUAL === $this->_operator)
-      return $model[$this->_key] <= $this->_getValueOrArray();
+class Storm_Model_PersistenceStrategy_ClauseLimit
+  extends Storm_Model_PersistenceStrategy_Clause {
+
+  public function assemble($select) : self {
+    $select->limit($this->getValueOrArray());
+    return $this;
+  }
+
+
+  public function getFormatDb($table) : string {
+    return '';
+  }
+
+
+  public function containAttibuteInVolatile(array $model) : bool {
+    return true;
+  }
+}
+
+
+
+
+class Storm_Model_PersistenceStrategy_ClauseLimitPage
+  extends Storm_Model_PersistenceStrategy_Clause {
+
+  public function getValueOrArray() {
+    return $this->_value_or_array ?? [];
+  }
+
+
+  public function assemble($select) : self {
+    $range = $this->getValueOrArray();
+    if (2 <= count($range))
+      $select->limitPage($range[0], $range[1]);
+
+    return $this;
+  }
+
+
+  public function getFormatDb($table) : string {
+    return '';
+  }
+
+
+  public function containAttibuteInVolatile(array $model) : bool {
+    return true;
+  }
+}
+
+
+
+
+class Storm_Model_PersistenceStrategy_ClauseOrderBy
+  extends Storm_Model_PersistenceStrategy_Clause {
+
+  const ORDER_DESC = ' desc';
+  protected ?string $_order_mode = null;
+
+  public function getValueOrArray() {
+    return $this->_value_or_array ?? null;
+  }
+
+
+  public function assemble($select) : self {
+    $select->order((($clause = $this->getValueOrArray())
+                    ? $clause->getFormatDb($select->getTable())
+                    : $this->_key)
+                   . $this->_order_mode);
+    return $this;
+  }
+
+
+  public function getFormatDb($table) : string {
+    return '';
+  }
+
+
+  public function containAttibuteInVolatile(array $model) : bool {
+    return true;
+  }
+
+
+  public function setOrder(bool $mode) : self {
+    $this->_order_mode = $mode ? static::ORDER_DESC : '';
+    return $this;
+  }
+
+
+  public function compare(Storm_Query_Order $query_order, array $a, array $b) : int {
+    $compare = $this->_compareValues($a, $b)
+      * $this->_descendant()
+      * $query_order->getPosition();
+
+    return ($next_order = $query_order->getNextQuery())
+      ? ($compare + $next_order->getClause()->compare($next_order, $a, $b))
+      : $compare;
+  }
+
+
+  protected function _compareValues(array $a, array $b) : int {
+    if ($match_clause = $this->getValueOrArray())
+      return $match_clause->compare($a, $b);
+
+    $first = $this->_existKeyInModel($a) ? $a[$this->_key] : '';
+    $second = $this->_existKeyInModel($b) ? $b[$this->_key] : '';
+
+    return ($first <=> $second);
+  }
+
 
-    return ($this->_negated !== ($model[$this->_key] == $this->_getValueOrArray()));
+  protected function _descendant() : int {
+    return static::ORDER_DESC === $this->_order_mode ? -1 : 1;
   }
 }
diff --git a/src/Storm/Model/PersistenceStrategy/Db.php b/src/Storm/Model/PersistenceStrategy/Db.php
index cc5b23a87b8ec769330a2b810df832c367be01b0..c5157b57bca4c9ec2d1776f004f03e223d9a730b 100644
--- a/src/Storm/Model/PersistenceStrategy/Db.php
+++ b/src/Storm/Model/PersistenceStrategy/Db.php
@@ -96,7 +96,8 @@ class Storm_Model_PersistenceStrategy_Db
   }
 
 
-  protected function _generateWhereClauseForKeyAndValue(string $key, $value) : string {
+  protected function _generateWhereClauseForKeyAndValue(string $key,
+                                                        $value) : string {
     $clause = ($value instanceof Storm_Model_PersistenceStrategy_Clause)
       ? $value
       : Storm_Model_PersistenceStrategy_Clause::newWith($key, $value);
@@ -170,6 +171,9 @@ class Storm_Model_PersistenceStrategy_Db
    * @see findAllBy
    */
   public function _generateSelectFor($args) {
+    if ($args instanceof Storm_Query)
+      return $args->assembleFor($this->getTable()->select());
+
     if (array_key_exists('role', $args) && array_key_exists('model', $args)) {
       $model = $args['model'];
       $role = $args['role'];
@@ -185,26 +189,34 @@ class Storm_Model_PersistenceStrategy_Db
     else
       $select = $this->getTable()->select();
 
-    foreach ($args as $field => $value) {
-      if (in_array($field, ['order', 'limit', 'where'], true)) {
-        $select->$field($value);
-        continue;
-      }
+    foreach ($args as $field => $value)
+      $this->_addInSelect($select, $field, $value);
 
-      if (in_array($field, ['limitPage', 'scope'], true)) {
-        $this->$field($select, $value);
-        continue;
-      }
+    return $select;
+  }
 
-      if ('group_by' === $field) {
-        $select->group($value);
-        continue;
-      }
 
-      $select->where($this->_generateWhereClauseForKeyAndValue($field, $value));
+  protected function _addInSelect($select, string $field, $value_or_clause) : self {
+    if (in_array($field, ['order', 'limit', 'where'], true)) {
+      $select->$field($value_or_clause);
+      return $this;
     }
 
-    return $select;
+    if (in_array($field, ['limitPage', 'scope'], true)) {
+      $this->$field($select, $value_or_clause);
+      return $this;
+    }
+
+    if ('group_by' === $field) {
+      $select->group($value_or_clause);
+      return $this;
+    }
+
+    if (!($value_or_clause instanceof Storm_Model_PersistenceStrategy_Clause))
+      $value_or_clause = Storm_Model_PersistenceStrategy_Clause::newWith($field, $value_or_clause);
+
+    $select->where($this->_generateWhereClauseForKeyAndValue($field, $value_or_clause));
+    return $this;
   }
 
 
diff --git a/src/Storm/Model/PersistenceStrategy/Match.php b/src/Storm/Model/PersistenceStrategy/Match.php
new file mode 100644
index 0000000000000000000000000000000000000000..e601ea22083d6a532c07f6f12cd4f234819e7d64
--- /dev/null
+++ b/src/Storm/Model/PersistenceStrategy/Match.php
@@ -0,0 +1,82 @@
+<?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_PersistenceStrategy_Match {
+
+  protected array $_terms;
+  protected bool $_boolean_mode;
+  protected bool $_strict;
+
+  public function __construct(array $terms, bool $strict, bool $boolean_mode) {
+    $this->_terms = $terms;
+    $this->_strict = $strict;
+    $this->_boolean_mode = $boolean_mode;
+  }
+
+
+  public function getFormatDb($table) : string {
+    $content = '';
+    foreach ($this->_terms as $key => $term)
+      $content = implode(' ', array_filter([$content,
+                                            $term->getFormatDb($this->_strict)]));
+
+    return sprintf('AGAINST(%s%s)',
+                   $table->getAdapter()->quoteInto('?', $content, null, null),
+                   ($this->_boolean_mode ? ' IN BOOLEAN MODE' : ''));
+  }
+
+
+  public function containVolatile(string $contents) : bool {
+    if (!$contents)
+      return false;
+
+    foreach ($this->_terms as $term) {
+      if ($this->_strict && !$term->containVolatile($contents))
+        return false;
+      if (!$this->_strict && $term->containVolatile($contents))
+        return true;
+    }
+
+    return $this->_strict;
+  }
+
+
+  public function getCompareValue(string $contents) : int {
+    $compare = 0;
+
+    foreach ($this->_terms as $term) {
+      $new_compare = $term->getCompareValue($contents);
+      if ($this->_strict && 0 === $new_compare)
+        return 0;
+
+      $compare += $term->getCompareValue($contents);
+    }
+
+    return $this->_boolean_mode
+      ? min($compare, 1)
+      : ceil(($compare / count(explode(' ', $contents))) * 10);
+  }
+}
diff --git a/src/Storm/Model/PersistenceStrategy/MatchTerms.php b/src/Storm/Model/PersistenceStrategy/MatchTerms.php
new file mode 100644
index 0000000000000000000000000000000000000000..bb5eb3032244eb3a00289989c3dc6af5ec44b48f
--- /dev/null
+++ b/src/Storm/Model/PersistenceStrategy/MatchTerms.php
@@ -0,0 +1,80 @@
+<?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
+gof 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_PersistenceStrategy_MatchTerms {
+
+  protected array $_values;
+  protected bool $_exact;
+
+  public function __construct($array_or_value, ?bool $exact = false) {
+    if (!is_array($array_or_value))
+      $array_or_value = explode(' ', $array_or_value);
+
+    $this->_values = $array_or_value;
+    $this->_exact = $exact ?? false;
+  }
+
+
+  public function getFormatDb(bool $strict) : string {
+    $values = implode(' ', $this->_values);
+    if ($this->_exact)
+      return $strict ? '+"' . $values . '"' : '"' . $values . '"';
+
+    return $strict ? '+(' . $values . ')' : $values;
+  }
+
+
+  public function containVolatile(string $contents) : bool {
+    if ($this->_exact)
+      return $this->_containValue($contents, implode(' ', $this->_values));
+
+    foreach ($this->_values as $value)
+      if ($this->_containValue($contents, $value))
+        return true;
+
+    return false;
+  }
+
+
+  public function getCompareValue(string $contents) : int {
+    if ($this->_exact)
+      return preg_match_all('/\b' . implode(' ', $this->_values) . '\b/', $contents);
+
+    $compare = 0;
+    foreach ($this->_values as $value)
+      $compare += ($new_compare = preg_match_all('/\b' . $value . '\b/', $contents))
+        ? $new_compare
+        : 0;
+
+    return $compare;
+  }
+
+
+  protected function _containValue(string $contents, string $value) : bool {
+    return preg_match('/\b' . $value . '\b/', $contents);
+  }
+}
diff --git a/src/Storm/Model/PersistenceStrategy/Volatile.php b/src/Storm/Model/PersistenceStrategy/Volatile.php
index 30344a661493dd7353604c80b4449a554f89983f..7abb84075309d1a1d085dacfb1250aa6bd9f05fc 100644
--- a/src/Storm/Model/PersistenceStrategy/Volatile.php
+++ b/src/Storm/Model/PersistenceStrategy/Volatile.php
@@ -27,8 +27,7 @@ THE SOFTWARE.
 class Storm_Model_PersistenceStrategy_Volatile  extends Storm_Model_PersistenceStrategy_Abstract {
   protected
     $_instances = [],
-    $desc_order=false,
-    $special_select_fields = ['order', 'limit', 'limitpage', 'where', 'group_by'],
+    $desc_order = false,
     $_table;
 
 
@@ -44,26 +43,23 @@ class Storm_Model_PersistenceStrategy_Volatile  extends Storm_Model_PersistenceS
 
 
   public function findAll($select = null) {
-    if (null === $filtered_select = $this->extractRoleAndModel(is_array($select)
-                                                               ? array_change_key_case($select)
-                                                               : []))
+    if (null === $filtered = (Storm_Model_PersistenceStrategy_Volatile_Filtered::newFor($select)
+                              ->extractRoleAndModel($this->_loader)))
       return [];
 
-    $group_by = $filtered_select['group_by'] ?? '';
-    $order = $filtered_select['order'] ?? '';
-    $limit = $filtered_select['limit'] ?? '';
+    $group_by = $filtered->getGroupBy();
+    $order = $filtered->getOrder();
+    $limit = $filtered->getLimit();
+    $limit_page = $filtered->getLimitPage();
 
     $page_size=0;
-    if (isset($filtered_select['limitpage'])) {
-      list($page, $page_size) = $filtered_select['limitpage'];
+    if ($limit_page) {
+      list($page, $page_size) = $limit_page;
       if ($page > 0) $page -= 1;
     }
 
-    foreach($this->special_select_fields as $field)
-      unset($filtered_select[$field]);
-
     $values = [];
-    $this->_allMatchingInstancesDo($filtered_select,
+    $this->_allMatchingInstancesDo($filtered->getClauses(),
                                    function($model) use (&$values) {
                                      $values []= $model;
                                    });
@@ -72,7 +68,7 @@ class Storm_Model_PersistenceStrategy_Volatile  extends Storm_Model_PersistenceS
     $values = $this->ordered($values, $order);
     $values = $this->limited($values, $limit);
 
-    if ($page_size>0)
+    if ($page_size > 0)
       $values = array_slice($values, $page * $page_size, $page_size);
 
     return array_map([$this->_loader, 'newFromRow'],
@@ -116,28 +112,6 @@ class Storm_Model_PersistenceStrategy_Volatile  extends Storm_Model_PersistenceS
   }
 
 
-  protected function extractRoleAndModel($select) {
-    if (array_key_exists('role', $select) && array_key_exists('model', $select)) {
-      $model = $select['model'];
-      $role = $select['role'];
-      unset($select['model']);
-      unset($select['role']);
-
-      if ($model->isNew()) return null;
-
-      $field = $this->_loader->getIdFieldForDependent($role);
-      $select[$field]=$model->getId();
-    }
-
-    if (array_key_exists('scope', $select)) {
-      $select = array_merge($select, array_change_key_case($select['scope']));
-      unset($select['scope']);
-    }
-    return $select;
-  }
-
-
-
   protected function getInstancesArray() : array {
     return
       array_values(
@@ -162,6 +136,9 @@ class Storm_Model_PersistenceStrategy_Volatile  extends Storm_Model_PersistenceS
 
 
   public function ordered($result, $select) {
+    if ($select instanceof Storm_Query)
+      return $select->orderVolatile($result);
+
     if (! ($select && $result) )
       return $result;
 
@@ -216,8 +193,8 @@ class Storm_Model_PersistenceStrategy_Volatile  extends Storm_Model_PersistenceS
       : null;
 
     if (!$clause && preg_match('/left\((.+),(.+)\)/', $key, $matches))
-      $clause = Storm_Model_PersistenceStrategy_Clause::clauseLike(trim($matches[1]),
-                                                                   (substr($value, 0, (int)$matches[2]) . '%'));
+      $clause = Storm_Model_PersistenceStrategy_Clause::like(trim($matches[1]),
+                                                             (substr($value, 0, (int)$matches[2]) . '%'));
 
     if (!$clause)
       $clause = Storm_Model_PersistenceStrategy_Clause::newWith($key, $value);
diff --git a/src/Storm/Model/PersistenceStrategy/Volatile/Filtered.php b/src/Storm/Model/PersistenceStrategy/Volatile/Filtered.php
new file mode 100644
index 0000000000000000000000000000000000000000..0baff2c0cd71bd064e26e17740b47bf47dcc0f3b
--- /dev/null
+++ b/src/Storm/Model/PersistenceStrategy/Volatile/Filtered.php
@@ -0,0 +1,121 @@
+<?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_PersistenceStrategy_Volatile_Filtered {
+
+  protected ?Storm_Query $_query;
+  protected ?array $_select;
+
+  public function __construct(?Storm_Query $query, ?array $select) {
+    $this->_query = $query;
+    $this->_select = $select;
+  }
+
+
+  public static function newFor($select) : self {
+    if ($select instanceof Storm_Query)
+      return (new static($select, null));
+
+    $select = is_array($select)
+      ? array_change_key_case($select)
+      : [];
+    return (new static(null, $select));
+  }
+
+
+  public function extractRoleAndModel(Storm_Model_Loader $loader) : self {
+    if ($this->_query)
+      return $this;
+
+    if (array_key_exists('role', $this->_select) && array_key_exists('model', $this->_select)) {
+      $model = $this->_select['model'];
+      $role = $this->_select['role'];
+      unset($this->_select['model']);
+      unset($this->_select['role']);
+
+      if ($model->isNew())
+        return null;
+
+      $field = $loader->getIdFieldForDependent($role);
+      $this->_select[$field] = $model->getId();
+    }
+
+    if (array_key_exists('scope', $this->_select)) {
+      $this->_select = array_merge($this->_select, array_change_key_case($this->_select['scope']));
+      unset($this->_select['scope']);
+    }
+
+    return $this;
+  }
+
+
+  public function getClauses() : array {
+    return $this->_query
+      ? $this->_query->getClauses()
+      : $this->_select;
+  }
+
+
+  public function getLimit() {
+    if ($this->_query)
+      return $this->_query->getLimitValue();
+
+    $limit = $this->_select['limit'] ?? '';
+    unset($this->_select['limit']);
+    return $limit;
+  }
+
+
+  public function getLimitPage() {
+    if ($this->_query)
+      return $this->_query->getLimitPageValue();
+
+    $limit_page = $this->_select['limitpage'] ?? '';
+    unset($this->_select['limitpage']);
+    return $limit_page;
+  }
+
+
+  public function getOrder() {
+    if ($this->_query)
+      return $this->_query;
+
+    $order = $this->_select['order'] ?? '';
+    unset($this->_select['order']);
+    return $order;
+  }
+
+
+  public function getGroupBy() : string {
+    if (!$this->_select)
+      return '';
+
+    $group_by = $this->_select['group_by'] ?? '';
+    unset($this->_select['group_by']);
+    return $group_by;
+  }
+}
diff --git a/src/Storm/Model/PersistenceStrategy/Volatile/OrderClause.php b/src/Storm/Model/PersistenceStrategy/Volatile/OrderClause.php
index 7f65cbf5ec42648f2108870587e6c2140221537b..35efd2c68a1f37f899f500a9271a592e0bdb1210 100644
--- a/src/Storm/Model/PersistenceStrategy/Volatile/OrderClause.php
+++ b/src/Storm/Model/PersistenceStrategy/Volatile/OrderClause.php
@@ -34,12 +34,13 @@ class Storm_Model_PersistenceStrategy_Volatile_OrderClause {
     $clauses = array_reverse(array_filter(explode(',', strtolower($order))));
 
     $first_clause = new static(array_shift($clauses));
-    foreach($clauses as $clause) {
+    foreach($clauses as $clause)
       $first_clause = new static($clause, $first_clause);
-    }
+
     return $first_clause;
   }
 
+
   public function __construct($order, $next_clause = null) {
     $description = explode(' ', trim($order));
 
@@ -69,12 +70,12 @@ class Storm_Model_PersistenceStrategy_Volatile_OrderClause {
   }
 
 
-  public function int_compare($a , $b)  {
+  public function int_compare($a, $b) {
     return ($a == $b) ? 0 : (($a < $b) ? -1 : 1);
   }
 
 
-  public function string_compare($a, $b){
+  public function string_compare($a, $b) {
     return strcmp($a, $b);
   }
 }
diff --git a/src/Storm/Query.php b/src/Storm/Query.php
new file mode 100644
index 0000000000000000000000000000000000000000..c116a582e1ce2720365fbdc1591774a26d310baa
--- /dev/null
+++ b/src/Storm/Query.php
@@ -0,0 +1,357 @@
+<?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 {
+
+  protected array $_clauses;
+  protected array $_orders;
+  protected array $_terms;
+  protected Storm_Model_Loader $_loader;
+  protected ?Storm_Model_PersistenceStrategy_Clause $_clause_limit;
+  protected ?Storm_Model_PersistenceStrategy_Clause $_clause_limit_page;
+
+  public function __construct() {
+    $this->_clauses = [];
+    $this->_orders = [];
+    $this->_terms = [];
+    $this->_clause_limit = null;
+    $this->_clause_limit_page = null;
+  }
+
+
+  public function __call($method, $args) {
+    return $this->_callQuery($method, $args);
+  }
+
+
+  public static function __callStatic($method, $args) {
+    return (new static)->_callQuery($method, $args);
+  }
+
+
+  protected function _callQuery($method, $args) {
+    if (!method_exists($this , '_' . $method))
+      throw new Exception('Call undefined method ' . $method);
+
+    return $this->{'_' . $method}(...$args);
+  }
+
+
+  public function assembleFor($select) {
+    foreach ($this->getClauses() as $clause)
+      $clause->assemble($select);
+
+    foreach ($this->getOrders() 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);
+
+    return $select;
+  }
+
+
+  public function orderVolatile(array $models) : array {
+    return (new Storm_Query_Order(array_reverse($this->getOrders())))
+      ->compareVolatile($models);
+  }
+
+
+  public function getClauses() : array {
+    return $this->_clauses;
+  }
+
+
+  public function getOrders() : array {
+    return $this->_orders;
+  }
+
+
+  public function getTerms() : array {
+    return $this->_terms;
+  }
+
+
+  public function getLimitValue() {
+    return $this->_clause_limit ? $this->_clause_limit->getValueOrArray() : '';
+  }
+
+
+  public function getLimitPageValue() : ?array {
+    return $this->_clause_limit_page ? $this->_clause_limit_page->getValueOrArray() : null;
+  }
+
+
+  protected function _limit($limit) : self {
+    $this->_clause_limit = Storm_Model_PersistenceStrategy_Clause::limit($limit);
+    return $this;
+  }
+
+
+  protected function _limit_page(array $range) {
+    $this->_clause_limit_page = Storm_Model_PersistenceStrategy_Clause::limit_page($range);
+    return $this;
+  }
+
+
+  protected function _eq(string $key, string $value) : self {
+    $this->_clauses [] = Storm_Model_PersistenceStrategy_Clause::equal($key, $value);
+    return $this;
+  }
+
+
+  public function _not_eq(string $key, string $value) : self {
+    $this->_clauses [] = Storm_Model_PersistenceStrategy_Clause::equal($key, $value)->setNegated(true);
+    return $this;
+  }
+
+
+  protected function _like(string $key, string $value) : self {
+    $this->_clauses [] = Storm_Model_PersistenceStrategy_Clause::like($key, $value);
+    return $this;
+  }
+
+
+  protected function _not_like(string $key, string $value) : self {
+    $this->_clauses [] = Storm_Model_PersistenceStrategy_Clause::like($key, $value)->setNegated(true);
+    return $this;
+  }
+
+
+  protected function _gt(string $key, string $value) : self {
+    $this->_clauses [] = Storm_Model_PersistenceStrategy_Clause::greater($key, $value);
+    return $this;
+  }
+
+
+  protected function _gt_eq(string $key, string $value) : self {
+    $this->_clauses [] = Storm_Model_PersistenceStrategy_Clause::greaterEqual($key, $value);
+    return $this;
+  }
+
+
+  protected function _lt(string $key, string $value) : self {
+    $this->_clauses [] = Storm_Model_PersistenceStrategy_Clause::lesser($key, $value);
+    return $this;
+  }
+
+
+  protected function _lt_eq(string $key, string $value) : self {
+    $this->_clauses [] = Storm_Model_PersistenceStrategy_Clause::lesserEqual($key, $value);
+    return $this;
+  }
+
+
+  protected function _is(string $key) : self {
+    $this->_clauses [] = Storm_Model_PersistenceStrategy_Clause::is($key);
+    return $this;
+  }
+
+
+  protected function _not_is(string $key) : self {
+    $this->_clauses [] = Storm_Model_PersistenceStrategy_Clause::is($key)->setNegated(true);
+    return $this;
+  }
+
+
+  protected function _in(string $key, array $array) : self {
+    $this->_clauses [] = Storm_Model_PersistenceStrategy_Clause::in($key, $array);
+    return $this;
+  }
+
+
+  protected function _not_in(string $key, array $array) : self {
+    $this->_clauses [] = Storm_Model_PersistenceStrategy_Clause::in($key, $array)->setNegated(true);
+    return $this;
+  }
+
+
+  protected function _start(string $key, string $value) : self {
+    $this->_clauses [] = Storm_Model_PersistenceStrategy_Clause::start($key, $value);
+    return $this;
+  }
+
+
+  protected function _end(string $key, string $value) : self {
+    $this->_clauses [] = Storm_Model_PersistenceStrategy_Clause::end($key, $value);
+    return $this;
+  }
+
+
+  protected function _or(Storm_Query ...$querys) : self {
+    $clauses = [];
+    foreach ($querys as $q)
+      $clauses = [...$clauses, ...$q->getClauses()];
+    $this->_clauses [] = Storm_Model_PersistenceStrategy_Clause::or($clauses);
+    return $this;
+  }
+
+
+  protected function _and(Storm_Query ...$querys) : self {
+    $clauses = [];
+    foreach ($querys as $q)
+      $clauses = [...$clauses, ...$q->getClauses()];
+    $this->_clauses [] = Storm_Model_PersistenceStrategy_Clause::and($clauses);
+    return $this;
+  }
+
+
+  protected function _match(string $key,
+                            Storm_Query $query,
+                            ?bool $strict = false,
+                            ?bool $boolean_mode = true) : self {
+    $this->_clauses [] = Storm_Model_PersistenceStrategy_Clause::match($key,
+                                                                       $query->getTerms(),
+                                                                       $strict,
+                                                                       $boolean_mode);
+    return $this;
+  }
+
+
+  protected function _terms($value_or_array, ?bool $exact = false) : self {
+    $this->_terms [] = (new Storm_Model_PersistenceStrategy_MatchTerms($value_or_array, $exact));
+    return $this;
+  }
+
+
+  protected function _order(string $key) : self {
+    $this->_orders [] = Storm_Model_PersistenceStrategy_Clause::order($key)->setOrder(false);
+    return $this;
+  }
+
+
+  protected function _order_desc(string $key) : self {
+    $this->_orders [] = Storm_Model_PersistenceStrategy_Clause::order($key)->setOrder(true);
+    return $this;
+  }
+
+
+  protected function _order_match(string $key,
+                                  Storm_Query $query,
+                                  ?bool $strict = false,
+                                  ?bool $boolean_mode = true) : self {
+    $match = Storm_Model_PersistenceStrategy_Clause::match($key,
+                                                           $query->getTerms(),
+                                                           $strict,
+                                                           $boolean_mode);
+    $this->_orders [] = Storm_Model_PersistenceStrategy_Clause::order($key, $match)->setOrder(false);
+    return $this;
+  }
+
+
+  protected function _order_match_desc(string $key,
+                                       Storm_Query $query,
+                                       ?bool $strict = false,
+                                       ?bool $boolean_mode = true) : self {
+    $match = Storm_Model_PersistenceStrategy_Clause::match($key,
+                                                           $query->getTerms(),
+                                                           $strict,
+                                                           $boolean_mode);
+    $this->_orders [] = Storm_Model_PersistenceStrategy_Clause::order($key, $match)->setOrder(true);
+    return $this;
+  }
+
+
+  protected function _from(Storm_Model_Loader $loader) : self {
+    $this->_loader = $loader;
+    return $this;
+  }
+
+
+  protected function _fetchAll() : array {
+    return $this->_loader
+      ? $this->_loader->getPersistenceStrategy()->findAllBy($this)
+      : [];
+  }
+
+
+  protected function _count() : array {
+    return $this->_loader
+      ? $this->_loader->getPersistenceStrategy()->countBy($this)
+      : [];
+  }
+
+
+  protected function _fetchFirst() : Storm_Model_Abstract {
+    return $this->_loader
+      ? $this->_loader->getPersistenceStrategy()->findFirstBy($this)
+      : null;
+  }
+}
+
+
+
+
+class Storm_Query_Order {
+
+  protected ?Storm_Query_Order $_next_query;
+  protected ?Storm_Model_PersistenceStrategy_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_Model_PersistenceStrategy_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/tests/Storm/Test/LoaderTest.php b/tests/Storm/Test/LoaderTest.php
index e7ab7352007a52b1be079896e130fe46807df2ad..d30618935d4a0dcf4d1768d747ddd92e2ea6e647 100644
--- a/tests/Storm/Test/LoaderTest.php
+++ b/tests/Storm/Test/LoaderTest.php
@@ -1,26 +1,26 @@
 <?php
 /*
-STORM is under the MIT License (MIT)
-
-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:
-
-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.
+  STORM is under the MIT License (MIT)
+
+  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:
+
+  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.
 */
 
 
@@ -38,15 +38,15 @@ abstract class Storm_Test_LoaderTestCase extends Storm_Test_ModelTestCase {
 
       ->whenCalled('quoteInto')
       ->willDo(function($clause, $value)
-               {
-                 return str_replace('?',
-                                    is_array($value)
-                                    ? implode(',',
-                                              array_map(function($item){ return "'" . $item . "'"; },
-                                                        $value))
-                                    :  "'" . $value . "'",
-                                    $clause);
-               });
+      {
+        return str_replace('?',
+                           is_array($value)
+                           ? implode(',',
+                                     array_map(function($item){ return "'" . $item . "'"; },
+                                               $value))
+                           :  "'" . $value . "'",
+                           $clause);
+      });
 
     $this->_loader = Storm_Test_Mock_User::getLoader()->setTable($this->_table);
   }
@@ -168,31 +168,6 @@ class Storm_Test_LoaderBasicTest extends Storm_Test_LoaderTestCase {
        [ ['where' => 'id=\'29\' and name=\'lp\''], ['id=\'29\' and name=\'lp\''] ],
        [ ['left(name, 5)' => 'ganda'], ['left(name, 5)=\'ganda\''] ],
        [ ['id' => '29'], ['id=\'29\''] ],
-       [ [ Storm_Model_PersistenceStrategy_Clause::clauseGreater('id', '30') ],
-        ['id>\'30\''] ],
-       [ [ Storm_Model_PersistenceStrategy_Clause::clauseGreaterEqual('id', '30') ],
-        ['id>=\'30\''] ],
-       [ [ Storm_Model_PersistenceStrategy_Clause::clauseLesser('id', '30') ],
-        ['id<\'30\''] ],
-       [ [ Storm_Model_PersistenceStrategy_Clause::clauseLesserEqual('id', '30') ],
-        ['id<=\'30\''] ],
-       [ [ Storm_Model_PersistenceStrategy_Clause::clauseIs('name') ], ['name is null'] ],
-       [ [ Storm_Model_PersistenceStrategy_Clause::clauseIn('name', ['Harlock', 'Nausicaa']) ],
-        ['name in (\'Harlock\',\'Nausicaa\')'] ],
-       [ [ Storm_Model_PersistenceStrategy_Clause::clauseNotEqual('name', 'Harlock') ],
-        ['name!=\'Harlock\''] ],
-       [ [ Storm_Model_PersistenceStrategy_Clause::clauseNotIn('name', ['Harlock', 'Nausicaa']) ],
-        ['name not in (\'Harlock\',\'Nausicaa\')'] ],
-       [ [ Storm_Model_PersistenceStrategy_Clause::clauseLike('login', '%aus%') ],
-        ['login like \'%aus%\''] ],
-       [ [ Storm_Model_PersistenceStrategy_Clause::clauseNotLike('login', '%aus%') ],
-        ['login not like \'%aus%\''] ],
-       [ [ Storm_Model_PersistenceStrategy_Clause::clauseEqual('left(name, 5)', 'ganda') ],
-        ['left(name, 5)=\'ganda\''] ],
-       [ [ Storm_Model_PersistenceStrategy_Clause::clauseStart('login', 'aus') ],
-        ['login like \'aus%\''] ],
-       [ [ Storm_Model_PersistenceStrategy_Clause::clauseEnd('login', 'aus') ],
-        ['login like \'%aus\''] ],
       ];
   }
 
@@ -360,3 +335,397 @@ class Storm_Test_LoaderSaveWithIdTest extends Storm_Test_LoaderTestCase {
                              $this->_table->getFirstAttributeForLastCallOn('update'));
   }
 }
+
+
+
+
+class Storm_Test_LoaderQueryTest extends Storm_Test_LoaderTestCase {
+
+  protected $_select;
+
+  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('where')->answers($this->_select)
+                  ->whenCalled('getTable')->answers($this->_table);
+  }
+
+
+  /** @test */
+  public function withClauseGreaterThanShouldReturnQuery() {
+    Storm_Query::gt('id', '30')
+      ->from($this->_loader)
+      ->fetchAll();
+
+    $this->assertEquals(['id>\'30\''],
+                        $this->_select->getAttributesForLastCallOn('where'));
+  }
+
+
+  /** @test */
+  public function withClauseGreaterThanEqualShouldReturnQuery() {
+    Storm_Query::gt_eq('id', '30')
+      ->from($this->_loader)
+      ->fetchAll();
+
+    $this->assertEquals(['id>=\'30\''],
+                        $this->_select->getAttributesForLastCallOn('where'));
+  }
+
+
+  /** @test */
+  public function withClauseLesserThanShouldReturnQuery() {
+    Storm_Query::lt('id', '30')
+      ->from($this->_loader)
+      ->fetchAll();
+
+    $this->assertEquals(['id<\'30\''],
+                        $this->_select->getAttributesForLastCallOn('where'));
+  }
+
+
+  /** @test */
+  public function withClauseLesserThanEqualShouldReturnQuery() {
+    Storm_Query::lt_eq('id', '30')
+      ->from($this->_loader)
+      ->fetchAll();
+
+    $this->assertEquals(['id<=\'30\''],
+                        $this->_select->getAttributesForLastCallOn('where'));
+  }
+
+
+  /** @test */
+  public function withClauseIsShouldReturnQuery() {
+    Storm_Query::is('name')
+      ->from($this->_loader)
+      ->fetchAll();
+
+    $this->assertEquals(['name is null'],
+                        $this->_select->getAttributesForLastCallOn('where'));
+  }
+
+
+  /** @test */
+  public function withClauseIsNotShouldReturnQuery() {
+    Storm_Query::not_is('name')
+      ->from($this->_loader)
+      ->fetchAll();
+
+    $this->assertEquals(['name is not null'],
+                        $this->_select->getAttributesForLastCallOn('where'));
+  }
+
+
+  /** @test */
+  public function withClauseInShouldReturnQuery() {
+    Storm_Query::in('name', ['Harlock', 'Nausicaa'])
+      ->from($this->_loader)
+      ->fetchAll();
+
+    $this->assertEquals(['name in (\'Harlock\',\'Nausicaa\')'],
+                        $this->_select->getAttributesForLastCallOn('where'));
+  }
+
+
+  /** @test */
+  public function withClauseNotInShouldReturnQuery() {
+    Storm_Query::not_in('name', ['Harlock', 'Nausicaa'])
+      ->from($this->_loader)
+      ->fetchAll();
+
+    $this->assertEquals(['name not in (\'Harlock\',\'Nausicaa\')'],
+                        $this->_select->getAttributesForLastCallOn('where'));
+  }
+
+
+  /** @test */
+  public function withClauseEqualShouldReturnQuery() {
+    Storm_Query::eq('left(name, 5)', 'ganda')
+      ->from($this->_loader)
+      ->fetchAll();
+
+    $this->assertEquals(['left(name, 5)=\'ganda\''],
+                        $this->_select->getAttributesForLastCallOn('where'));
+  }
+
+
+  /** @test */
+  public function withClauseNotEqualShouldReturnQuery() {
+    Storm_Query::not_eq('name', 'Harlock')
+      ->from($this->_loader)
+      ->fetchAll();
+
+    $this->assertEquals(['name!=\'Harlock\''],
+                        $this->_select->getAttributesForLastCallOn('where'));
+  }
+
+
+  /** @test */
+  public function withClauseLikeShouldReturnQuery() {
+    Storm_Query::like('login', '%aus%')
+      ->from($this->_loader)
+      ->fetchAll();
+
+    $this->assertEquals(['login like \'%aus%\''],
+                        $this->_select->getAttributesForLastCallOn('where'));
+  }
+
+
+  /** @test */
+  public function withClauseNotLikeShouldReturnQuery() {
+    Storm_Query::not_like('login', '%aus%')
+      ->from($this->_loader)
+      ->fetchAll();
+
+    $this->assertEquals(['login not like \'%aus%\''],
+                        $this->_select->getAttributesForLastCallOn('where'));
+  }
+
+
+  /** @test */
+  public function withClauseStartShouldReturnQuery() {
+    Storm_Query::start('login', 'aus')
+      ->from($this->_loader)
+      ->fetchAll();
+
+    $this->assertEquals(['login like \'aus%\''],
+                        $this->_select->getAttributesForLastCallOn('where'));
+  }
+
+
+  /** @test */
+  public function withClauseEndShouldReturnQuery() {
+    Storm_Query::end('login', 'aus')
+      ->from($this->_loader)
+      ->fetchAll();
+
+    $this->assertEquals(['login like \'%aus\''],
+                        $this->_select->getAttributesForLastCallOn('where'));
+  }
+
+
+  /** @test */
+  public function withClauseOrShouldReturnQuery() {
+    Storm_Query::or(Storm_Query::eq('login', 'aus'),
+                    Storm_Query::in('role', ['admin', 'invite']))
+      ->from($this->_loader)
+      ->fetchAll();
+
+    $this->assertEquals(['(login=\'aus\' or role in (\'admin\',\'invite\'))'],
+                        $this->_select->getAttributesForLastCallOn('where'));
+  }
+
+
+  /** @test */
+  public function withClauseAndShouldReturnQuery() {
+    Storm_Query::and(Storm_Query::eq('login', 'aus')->in('role', ['admin', 'invite']))
+      ->from($this->_loader)
+      ->fetchAll();
+
+    $this->assertEquals(['(login=\'aus\' and role in (\'admin\',\'invite\'))'],
+                        $this->_select->getAttributesForLastCallOn('where'));
+  }
+
+
+  /** @test */
+  public function withClauseMatchShouldReturnQueryInBooleanMode() {
+    Storm_Query::match('login, role', Storm_Query::terms(['ADMIN', 'INVITE']))
+      ->from($this->_loader)
+      ->fetchAll();
+
+    $this->assertEquals(['MATCH(login, role) AGAINST(\'ADMIN INVITE\' IN BOOLEAN MODE)'],
+                        $this->_select->getAttributesForLastCallOn('where'));
+  }
+
+
+  /** @test */
+  public function withClauseMatchMultipleTermsShouldReturnQueryInBooleanMode() {
+    Storm_Query::match('login,role', Storm_Query::terms('ADMIN INVITE')->terms('HUGO ADRIEN'))
+      ->from($this->_loader)
+      ->fetchAll();
+
+    $this->assertEquals(['MATCH(login,role) AGAINST(\'ADMIN INVITE HUGO ADRIEN\' IN BOOLEAN MODE)'],
+                        $this->_select->getAttributesForLastCallOn('where'));
+  }
+
+
+  /** @test */
+  public function withClauseMatchStrictModeShouldRturnQueryInBooleanMode() {
+    Storm_Query::match('login,role', Storm_Query::terms('ADMIN INVITE')->terms('HUGO ADRIEN'),
+                       true)
+      ->from($this->_loader)
+      ->fetchAll();
+
+    $this->assertEquals(['MATCH(login,role) AGAINST(\'+(ADMIN INVITE) +(HUGO ADRIEN)\' IN BOOLEAN MODE)'],
+                        $this->_select->getAttributesForLastCallOn('where'));
+  }
+
+
+  /** @test */
+  public function withClauseMatchStrictAndOneExactExpressionWithNoBooleanModeShouldReturnQuery() {
+    Storm_Query::match('login,role', Storm_Query::terms('ADMIN INVITE', true)->terms('HUGO ADRIEN'),
+                       true,
+                       false)
+      ->from($this->_loader)
+      ->fetchAll();
+
+    $this->assertEquals(['MATCH(login,role) AGAINST(\'+"ADMIN INVITE" +(HUGO ADRIEN)\')'],
+                        $this->_select->getAttributesForLastCallOn('where'));
+  }
+}
+
+
+
+
+class Storm_Test_LoaderQueryLimitTest extends Storm_Test_LoaderTestCase {
+
+  protected $_select;
+
+  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);
+  }
+
+
+  /** @test */
+  public function withClauseLimit30ShouldReturnQuery() {
+    Storm_Query::limit(30)
+      ->from($this->_loader)
+      ->fetchAll();
+
+    $this->assertEquals([30],
+                        $this->_select->getAttributesForLastCallOn('limit'));
+  }
+
+
+  /** @test */
+  public function withClauseLimit30Offset10ShouldReturnQuery() {
+    Storm_Query::limit('10, 30')
+      ->from($this->_loader)
+      ->fetchAll();
+
+    $this->assertEquals(['10, 30'],
+                        $this->_select->getAttributesForLastCallOn('limit'));
+  }
+}
+
+
+
+
+class Storm_Test_LoaderQueryLimitPageTest extends Storm_Test_LoaderTestCase {
+
+  protected $_select;
+
+  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('limitPage')->answers($this->_select);
+  }
+
+
+  /** @test */
+  public function withClauseLimitPageShouldReturnQuery() {
+    Storm_Query::limit_page([1, 100])
+      ->from($this->_loader)
+      ->fetchAll();
+
+    $this->assertEquals([1, 100],
+                        $this->_select->getAttributesForLastCallOn('limitPage'));
+  }
+}
+
+
+
+
+class Storm_Test_LoaderQueryOrderTest extends Storm_Test_LoaderTestCase {
+
+  protected $_select;
+
+  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('order')->answers($this->_select)
+                  ->whenCalled('getTable')->answers($this->_table);
+  }
+
+
+  /** @test */
+  public function withClauseOrderIdShouldReturnOrderId() {
+    Storm_Query::order('id')
+      ->from($this->_loader)
+      ->fetchAll();
+
+    $this->assertEquals(['id'],
+                        $this->_select->getAttributesForLastCallOn('order'));
+  }
+
+
+  /** @test */
+  public function withClauseOrderIdDescShouldReturnOrderIdDesc() {
+    Storm_Query::order_desc('id')
+      ->from($this->_loader)
+      ->fetchAll();
+
+    $this->assertEquals(['id desc'],
+                        $this->_select->getAttributesForLastCallOn('order'));
+  }
+
+
+  /** @test */
+  public function withMultipleClauseOrderShouldReturnAllOrders() {
+    Storm_Query::order_desc('id')
+      ->order('name')
+      ->order_desc('role')
+      ->from($this->_loader)
+      ->fetchAll();
+
+    $this->assertEquals(['id desc', 'name', 'role desc'],
+                        [$this->_select->getFirstAttributeForMethodCallAt('order', 0),
+                         $this->_select->getFirstAttributeForMethodCallAt('order', 1),
+                         $this->_select->getFirstAttributeForMethodCallAt('order', 2)]);
+  }
+
+
+  /** @test */
+  public function withMatchClauseOrderShouldReturnMatchOrder() {
+    Storm_Query::order_match('login, role', Storm_Query::terms(['ADMIN', 'INVITE']), false, false)
+      ->from($this->_loader)
+      ->fetchAll();
+
+    $this->assertEquals(['MATCH(login, role) AGAINST(\'ADMIN INVITE\')'],
+                        $this->_select->getAttributesForLastCallOn('order'));
+  }
+
+
+  /** @test */
+  public function withMatchClauseOrderDescShouldReturnMatchOrder() {
+    Storm_Query::order_match_desc('login, role', Storm_Query::terms(['ADMIN', 'INVITE']))
+      ->from($this->_loader)
+      ->fetchAll();
+
+    $this->assertEquals(['MATCH(login, role) AGAINST(\'ADMIN INVITE\' IN BOOLEAN MODE) desc'],
+                        $this->_select->getAttributesForLastCallOn('order'));
+  }
+}
diff --git a/tests/Storm/Test/LoaderVolatileTest.php b/tests/Storm/Test/LoaderVolatileTest.php
index 3e91fdf9f94fdd2e3b9bbaab7b14f7854ecb00be..09c2aa8f46d9cb52ed25a8338328303e6793f048 100644
--- a/tests/Storm/Test/LoaderVolatileTest.php
+++ b/tests/Storm/Test/LoaderVolatileTest.php
@@ -148,7 +148,6 @@ class Storm_Test_LoaderVolatileTest extends Storm_Test_ModelTestCase {
                                                        'level' => 'invite',
                                                        'brain_id' => null,
                                                        'id_mouth' => null]);
-
   }
 
 
@@ -608,6 +607,24 @@ class Storm_Test_LoaderVolatileTest extends Storm_Test_ModelTestCase {
   }
 
 
+  /** @test */
+  public function withClauseLimitOneShouldReturnOnlyUserOneAlbert() {
+    $this->assertEquals([$this->albert],
+                        Storm_Query::limit(1)
+                        ->from(Storm_Test_VolatileUser::getLoader())
+                        ->fetchAll());
+  }
+
+
+  /** @test */
+  public function withClauseLimitOneTwoShouldReturnTwoUserHubertAndZoe() {
+    $this->assertEquals([$this->hubert, $this->zoe],
+                        Storm_Query::limit('1, 2')
+                        ->from(Storm_Test_VolatileUser::getLoader())
+                        ->fetchAll());
+  }
+
+
   /** @test */
   public function deleteByLevelInviteShouldDeleteHubertAndMax() {
     Storm_Test_VolatileUser::deleteBy(['level' => 'invite']);
@@ -655,6 +672,33 @@ class Storm_Test_LoaderVolatileTest extends Storm_Test_ModelTestCase {
   }
 
 
+  /** @test */
+  public function withClauseLimitPageShouldPaginate() {
+    $this->assertEquals([$this->albert, $this->hubert],
+                        Storm_Query::from(Storm_Test_VolatileUser::getLoader())
+                        ->limit_page([0, 2])
+                        ->fetchAll());
+    $this->assertEquals([$this->albert, $this->hubert],
+                        Storm_Query::from(Storm_Test_VolatileUser::getLoader())
+                        ->limit_page([1, 2])
+                        ->fetchAll());
+    $this->assertEquals([$this->zoe],
+                        Storm_Query::from(Storm_Test_VolatileUser::getLoader())
+                        ->limit_page([2, 2])
+                        ->fetchAll());
+  }
+
+
+  /** @test */
+  public function withClauseLimitPageAndAttributeShouldWork() {
+    $this->assertEquals([$this->hubert, $this->zoe],
+                        Storm_Query::from(Storm_Test_VolatileUser::getLoader())
+                        ->eq('foo', 'snafu')
+                        ->limit_page([1, 2])
+                        ->fetchAll());
+  }
+
+
   /** @test */
   public function memoryReportShouldPrintThreeUsers() {
     $this->assertContains("Storm_Test_VolatileUser: 3",
@@ -782,7 +826,9 @@ class Storm_Test_LoaderVolatileTest extends Storm_Test_ModelTestCase {
   /** @test */
   public function selectAllCatsWhereIdMoreThanThreeShouldAnswersFifiAndLoulou() {
     $this->assertEquals(['fifi', 'loulou'],
-                        (new Storm_Model_Collection(Storm_Test_VolatileCat::findAllBy([Storm_Test_VolatileCat::clauseGreater('id', 3)])))
+                        (new Storm_Model_Collection(Storm_Query::gt('id', 3)
+                                                    ->from(Storm_Test_VolatileCat::getLoader())
+                                                    ->fetchAll()))
                         ->collect('name')
                         ->getArrayCopy());
   }
@@ -791,7 +837,9 @@ class Storm_Test_LoaderVolatileTest extends Storm_Test_ModelTestCase {
   /** @test */
   public function selectAllCatsWhereIdMoreOrEqualThanForShouldAnswersFifiAndLoulou() {
     $this->assertEquals(['fifi', 'loulou'],
-                        (new Storm_Model_Collection(Storm_Test_VolatileCat::findAllBy([Storm_Test_VolatileCat::clauseGreaterEqual('id', 4)])))
+                        (new Storm_Model_Collection(Storm_Query::gt_eq('id', 4)
+                                                    ->from(Storm_Test_VolatileCat::getLoader())
+                                                    ->fetchAll()))
                         ->collect('name')
                         ->getArrayCopy());
   }
@@ -800,7 +848,9 @@ class Storm_Test_LoaderVolatileTest extends Storm_Test_ModelTestCase {
   /** @test */
   public function selectAllCatsWhereIdLesserThanFiveShouldAnswersRiriAndFifi() {
     $this->assertEquals(['riri', 'fifi'],
-                        (new Storm_Model_Collection(Storm_Test_VolatileCat::findAllBy([Storm_Test_VolatileCat::clauseLesser('id', 5)])))
+                        (new Storm_Model_Collection(Storm_Query::lt('id', 5)
+                                                    ->from(Storm_Test_VolatileCat::getLoader())
+                                                    ->fetchAll()))
                         ->collect('name')
                         ->getArrayCopy());
   }
@@ -809,7 +859,9 @@ class Storm_Test_LoaderVolatileTest extends Storm_Test_ModelTestCase {
   /** @test */
   public function selectAllCatsWhereIdLesserOrEqualThanForShouldAnswersRiriAndFifi() {
     $this->assertEquals(['riri', 'fifi'],
-                        (new Storm_Model_Collection(Storm_Test_VolatileCat::findAllBy([Storm_Test_VolatileCat::clauseLesserEqual('id', 4)])))
+                        (new Storm_Model_Collection(Storm_Query::lt_eq('id', 4)
+                                                    ->from(Storm_Test_VolatileCat::getLoader())
+                                                    ->fetchAll()))
                         ->collect('name')
                         ->getArrayCopy());
   }
@@ -818,7 +870,9 @@ class Storm_Test_LoaderVolatileTest extends Storm_Test_ModelTestCase {
   /** @test */
   public function selectUserLoginNotHubertShouldReturnAlbertAndZoe() {
     $this->assertEquals(['albert', 'zoe'],
-                        (new Storm_Model_Collection(Storm_Test_VolatileUser::findAllBy([Storm_Test_VolatileUser::clauseNotEqual('login', 'hubert')])))
+                        (new Storm_Model_Collection(Storm_Query::not_eq('login', 'hubert')
+                                                    ->from(Storm_Test_VolatileUser::getLoader())
+                                                    ->fetchAll()))
                         ->collect('login')
                         ->getArrayCopy());
   }
@@ -827,7 +881,10 @@ class Storm_Test_LoaderVolatileTest extends Storm_Test_ModelTestCase {
   /** @test */
   public function selectUserLoginInAlbertZoeAndFooNotLikeSnafuShouldReturnAlbert() {
     $this->assertEquals(['albert'],
-                        (new Storm_Model_Collection(Storm_Test_VolatileUser::findAllBy([Storm_Test_VolatileUser::clauseIn('login', ['albert', 'zoe']), Storm_Test_VolatileUser::clauseNotLike('foo', '%naf%')])))
+                        (new Storm_Model_Collection(Storm_Query::in('login', ['albert', 'zoe'])
+                                                    ->not_like('foo', '%naf%')
+                                                    ->from(Storm_Test_VolatileUser::getLoader())
+                                                    ->fetchAll()))
                         ->collect('login')
                         ->getArrayCopy());
   }
@@ -836,7 +893,10 @@ class Storm_Test_LoaderVolatileTest extends Storm_Test_ModelTestCase {
   /** @test */
   public function selectUserLoginInAlbertZoeAndFooStartSnaShouldReturnZoe() {
     $this->assertEquals(['zoe'],
-                        (new Storm_Model_Collection(Storm_Test_VolatileUser::findAllBy([Storm_Test_VolatileUser::clauseIn('login', ['albert', 'zoe']), Storm_Test_VolatileUser::clauseStart('foo', 'sna')])))
+                        (new Storm_Model_Collection(Storm_Query::in('login', ['albert', 'zoe'])
+                                                    ->start('foo', 'sna')
+                                                    ->from(Storm_Test_VolatileUser::getLoader())
+                                                    ->fetchAll()))
                         ->collect('login')
                         ->getArrayCopy());
   }
@@ -845,7 +905,10 @@ class Storm_Test_LoaderVolatileTest extends Storm_Test_ModelTestCase {
   /** @test */
   public function selectUserLoginInAlbertZoeAndFooEndAfuShouldReturnZoe() {
     $this->assertEquals(['zoe'],
-                        (new Storm_Model_Collection(Storm_Test_VolatileUser::findAllBy([Storm_Test_VolatileUser::clauseIn('login', ['albert', 'zoe']), Storm_Test_VolatileUser::clauseEnd('foo', 'afu')])))
+                        (new Storm_Model_Collection(Storm_Query::in('login', ['albert', 'zoe'])
+                                                    ->end('foo', 'afu')
+                                                    ->from(Storm_Test_VolatileUser::getLoader())
+                                                    ->fetchAll()))
                         ->collect('login')
                         ->getArrayCopy());
   }
@@ -859,3 +922,237 @@ class Storm_Test_LoaderVolatileTest extends Storm_Test_ModelTestCase {
                         ->getArrayCopy());
   }
 }
+
+
+
+
+class Storm_Test_LoaderVolatileClauseWhereTest extends Storm_Test_ModelTestCase {
+
+  public function setUp() {
+    parent::setUp();
+
+    $this->fixture(Storm_Test_VolatileUser::class,
+                   ['id' => 100,
+                    'login' => 'user_admin',
+                    'level' => 'invite admin redacteur',
+                    'foo' => 'premier deuxieme troisieme']);
+
+    $this->fixture(Storm_Test_VolatileUser::class,
+                   ['id' => 101,
+                    'login' => 'user_administrateur',
+                    'level' => 'administrateur',
+                    'foo' => 'deuxieme redacteur']);
+
+    $this->fixture(Storm_Test_VolatileUser::class,
+                   ['id' => 102,
+                    'login' => 'user_invite',
+                    'level' => 'invite',
+                    'foo' => 'premier deuxieme']);
+  }
+
+
+  /** @test */
+  public function withMatchLevelAgainstAdminShouldAnswersOnlyUser_Admin() {
+    $this->assertEquals(['user_admin'],
+                        (new Storm_Model_Collection(Storm_Query::match('level', Storm_Query::terms(['admin']))
+                                                    ->from(Storm_Test_VolatileUser::getLoader())
+                                                    ->fetchAll()))
+                        ->collect('login')
+                        ->getArrayCopy());
+  }
+
+
+  /** @test */
+  public function withMatchLevelAgainstAdminDeuxiemeShouldAnswersUser_AdminAndUser_AdministrateurAndUser_Invite() {
+    $this->assertEquals(['user_admin', 'user_administrateur', 'user_invite'],
+                        (new Storm_Model_Collection(Storm_Query::match('level,foo',
+                                                                       Storm_Query::terms(['admin', 'deuxieme']))
+                                                    ->from(Storm_Test_VolatileUser::getLoader())
+                                                    ->fetchAll()))
+                        ->collect('login')
+                        ->getArrayCopy());
+  }
+
+
+  /** @test */
+  public function withMatchLevelAgainstStrictAdminRedacteur_DeuxiemeShouldAnswersUser_AdminAndUser_Administrateur() {
+    $this->assertEquals(['user_admin', 'user_administrateur'],
+                        (new Storm_Model_Collection(Storm_Query::match('level,foo',
+                                                                       Storm_Query::terms(['admin', 'redacteur'])
+                                                                       ->terms(['deuxieme']),
+                                                                       true)
+                                                    ->from(Storm_Test_VolatileUser::getLoader())
+                                                    ->fetchAll()))
+                        ->collect('login')
+                        ->getArrayCopy());
+  }
+
+
+  /** @test */
+  public function withMatchLevelAgainstExactDeuxiemeTroisiemeShouldAnswersOnlyUser_Admin() {
+    $this->assertEquals(['user_administrateur'],
+                        (new Storm_Model_Collection(Storm_Query::match('level,foo',
+                                                                       Storm_Query::terms('deuxieme redacteur',
+                                                                                          true))
+                                                    ->from(Storm_Test_VolatileUser::getLoader())
+                                                    ->fetchAll()))
+                        ->collect('login')
+                        ->getArrayCopy());
+  }
+
+
+  /** @test */
+  public function withOrClause_LevelInviteOrFooLikePremierShouldAnswersUser_AdminAndUser_Invite() {
+    $this->assertEquals(['user_admin', 'user_invite'],
+                        (new Storm_Model_Collection(Storm_Query::from(Storm_Test_VolatileUser::getLoader())
+                                                    ->or(Storm_Query::eq('level', 'invite')
+                                                         ->like('foo', '%premier%'))
+                                                    ->fetchAll()))
+                        ->collect('login')
+                        ->getArrayCopy());
+  }
+
+
+  /** @test */
+  public function withAndClause_LevelInviteOrFooLikePremierShouldAnswersOnlyUser_Invite() {
+    $this->assertEquals(['user_invite'],
+                        (new Storm_Model_Collection(Storm_Query::from(Storm_Test_VolatileUser::getLoader())
+                                                    ->and(Storm_Query::eq('level', 'invite')
+                                                         ->like('foo', '%premier%'))
+                                                    ->fetchAll()))
+                        ->collect('login')
+                        ->getArrayCopy());
+  }
+}
+
+
+
+
+class Storm_Test_LoaderVolatileClauseOrderTest extends Storm_Test_ModelTestCase {
+
+  public function setUp() {
+    parent::setUp();
+
+    $this->fixture(Storm_Test_VolatileUser::class,
+                   ['id' => 100,
+                    'login' => 'user_admin',
+                    'level' => 'invite admin redacteur',
+                    'foo' => 'first second third',
+                    'order1' => 'abcd',
+                    'order2' => 2,
+                    'order3' => 'defg']);
+
+    $this->fixture(Storm_Test_VolatileUser::class,
+                   ['id' => 101,
+                    'login' => 'user_administrateur',
+                    'level' => 'administrateur',
+                    'foo' => 'third second forth',
+                    'order1' => 'abcdef',
+                    'order2' => 3,
+                    'order3' => 'defg']);
+
+    $this->fixture(Storm_Test_VolatileUser::class,
+                   ['id' => 102,
+                    'login' => 'user_invite',
+                    'level' => 'invite',
+                    'foo' => 'forth fifth seven',
+                    'order1' => 'cdef',
+                    'order2' => 1,
+                    'order3' => 'klmn']);
+  }
+
+
+  /** @test */
+  public function with_Order1Desc_ShouldAnswersInOrdered() {
+    $this->assertEquals(['user_invite', 'user_administrateur', 'user_admin'],
+                        (new Storm_Model_Collection(Storm_Query::from(Storm_Test_VolatileUser::getLoader())
+                                                    ->order_desc('order1')
+                                                    ->fetchAll()))
+                        ->collect('login')
+                        ->getArrayCopy());
+  }
+
+
+  /** @test */
+  public function with_Order3Asc_Order2Desc_ShouldAnswersOrderedUser() {
+    $this->assertEquals(['user_administrateur', 'user_admin', 'user_invite'],
+                        (new Storm_Model_Collection(Storm_Query::from(Storm_Test_VolatileUser::getLoader())
+                                                    ->order('order3')
+                                                    ->order_desc('order2')
+                                                    ->fetchAll()))
+                        ->collect('login')
+                        ->getArrayCopy());
+  }
+
+
+  /** @test */
+  public function with_Order3Desc_Order2Desc_ShouldAnswersOrderedUser() {
+    $this->assertEquals(['user_invite', 'user_administrateur', 'user_admin'],
+                        (new Storm_Model_Collection(Storm_Query::from(Storm_Test_VolatileUser::getLoader())
+                                                    ->order_desc('order3')
+                                                    ->order_desc('order2')
+                                                    ->fetchAll()))
+                        ->collect('login')
+                        ->getArrayCopy());
+  }
+
+
+  /** @test */
+  public function withMatch_Level_Administrateur_ShouldAnswersOrderedUser() {
+    $query = Storm_Query::from(Storm_Test_VolatileUser::getLoader())
+      ->order_match('level', Storm_Query::terms('administrateur'),
+                         false, false);
+    $this->assertEquals(['user_admin', 'user_invite', 'user_administrateur'],
+                        (new Storm_Model_Collection($query->fetchAll()))
+                        ->collect('login')
+                        ->getArrayCopy());
+  }
+
+
+  /** @test */
+  public function withMatch_Level_Administrateur_Redacteur_Desc_ShouldAnswersOrderedUser() {
+    $query = Storm_Query::from(Storm_Test_VolatileUser::getLoader())
+      ->order_match_desc('level', Storm_Query::terms('administrateur redacteur'),
+                         false, false);
+    $this->assertEquals(['user_administrateur', 'user_admin', 'user_invite'],
+                        (new Storm_Model_Collection($query->fetchAll()))
+                        ->collect('login')
+                        ->getArrayCopy());
+  }
+
+
+  /** @test */
+  public function withMatchInBooleanMode_Level_Administrateur_Redacteur_Desc_ShouldAnswersOrderedUser() {
+    $query = Storm_Query::from(Storm_Test_VolatileUser::getLoader())
+      ->order_match_desc('level', Storm_Query::terms('administrateur redacteur'));
+    $this->assertEquals(['user_admin', 'user_administrateur', 'user_invite'],
+                        (new Storm_Model_Collection($query->fetchAll()))
+                        ->collect('login')
+                        ->getArrayCopy());
+  }
+
+
+  /** @test */
+  public function withMatch_FooDescExacteInBooleanMode_Forth_Fifth_Level_Administrateur_Redacteur_ShouldAnswersOrderedUser() {
+    $query = Storm_Query::from(Storm_Test_VolatileUser::getLoader())
+      ->order_match_desc('foo', Storm_Query::terms('forth fifth', true))
+      ->order_match('level', Storm_Query::terms('administrateur redacteur'),
+                    false, false);
+    $this->assertEquals(['user_invite', 'user_admin', 'user_administrateur'],
+                        (new Storm_Model_Collection($query->fetchAll()))
+                        ->collect('login')
+                        ->getArrayCopy());
+  }
+
+
+  /** @test */
+  public function withMatch_FooLevelStrict_InviteRedacteur_SecondThird_ShouldAnswersOrderedUser() {
+    $query = Storm_Query::from(Storm_Test_VolatileUser::getLoader())
+      ->order_match('foo,level', Storm_Query::terms('invite redacteur')
+                    ->terms('second third'), true);
+    $this->assertEquals(['user_administrateur', 'user_invite', 'user_admin'],
+                        (new Storm_Model_Collection($query->fetchAll()))
+                        ->collect('login')
+                        ->getArrayCopy());
+  }
+}