458 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			458 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?php
 | 
						|
namespace nur\sery\db\sqlite;
 | 
						|
 | 
						|
use nur\sery\cl;
 | 
						|
use nur\sery\str;
 | 
						|
use nur\sery\ValueException;
 | 
						|
use SQLite3;
 | 
						|
use SQLite3Stmt;
 | 
						|
 | 
						|
class _Query {
 | 
						|
  static function verifix(&$query, ?array &$params=null): void {
 | 
						|
    if (is_array($query)) {
 | 
						|
      $prefix = $query[0] ?? null;
 | 
						|
      if ($prefix === null) {
 | 
						|
        throw new ValueException("requête invalide");
 | 
						|
      } elseif (self::is_create($prefix)) {
 | 
						|
        $query = self::parse_create($query, $params);
 | 
						|
      } elseif (self::is_select($prefix)) {
 | 
						|
        $query = self::parse_select($query, $params);
 | 
						|
      } elseif (self::is_insert($prefix)) {
 | 
						|
        $query = self::parse_insert($query, $params);
 | 
						|
      } elseif (self::is_update($prefix)) {
 | 
						|
        $query = self::parse_update($query, $params);
 | 
						|
      } elseif (self::is_delete($prefix)) {
 | 
						|
        $query = self::parse_delete($query, $params);
 | 
						|
      } else {
 | 
						|
        throw SqliteException::wrap(ValueException::invalid_kind($query, "query"));
 | 
						|
      }
 | 
						|
    } elseif (!is_string($query)) {
 | 
						|
      $query = strval($query);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  static function consume(string $pattern, string &$string, ?array &$ms=null): bool {
 | 
						|
    if (!preg_match("/^$pattern/i", $string, $ms)) return false;
 | 
						|
    $string = substr($string, strlen($ms[0]));
 | 
						|
    return true;
 | 
						|
  }
 | 
						|
 | 
						|
  /** fusionner toutes les parties séquentielles d'une requête */
 | 
						|
  static function merge_seq(array $query): string {
 | 
						|
    $index = 0;
 | 
						|
    $sql = "";
 | 
						|
    foreach ($query as $key => $value) {
 | 
						|
      if ($key === $index) {
 | 
						|
        $index++;
 | 
						|
        if ($sql && !str::ends_with(" ", $sql) && !str::starts_with(" ", $value)) {
 | 
						|
          $sql .= " ";
 | 
						|
        }
 | 
						|
        $sql .= $value;
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return $sql;
 | 
						|
  }
 | 
						|
 | 
						|
  static function is_sep(&$cond): bool {
 | 
						|
    if (!is_string($cond)) return false;
 | 
						|
    if (!preg_match('/^\s*(and|or|not)\s*$/i', $cond, $ms)) return false;
 | 
						|
    $cond = $ms[1];
 | 
						|
    return true;
 | 
						|
  }
 | 
						|
 | 
						|
  static function parse_conds(?array $conds, ?array &$sql, ?array &$params): void {
 | 
						|
    if (!$conds) return;
 | 
						|
    $sep = null;
 | 
						|
    $index = 0;
 | 
						|
    $condsql = [];
 | 
						|
    foreach ($conds as $key => $cond) {
 | 
						|
      if ($key === $index) {
 | 
						|
        ## séquentiel
 | 
						|
        if ($index === 0 && self::is_sep($cond)) {
 | 
						|
          $sep = $cond;
 | 
						|
        } elseif (is_array($cond)) {
 | 
						|
          # condition récursive
 | 
						|
          self::parse_conds($cond, $condsql, $params);
 | 
						|
        } else {
 | 
						|
          # condition litérale
 | 
						|
          $condsql[] = strval($cond);
 | 
						|
        }
 | 
						|
        $index++;
 | 
						|
      } else {
 | 
						|
        ## associatif
 | 
						|
        # paramètre
 | 
						|
        $param = $key;
 | 
						|
        if ($params !== null && array_key_exists($param, $params)) {
 | 
						|
          $i = 1;
 | 
						|
          while (array_key_exists("$key$i", $params)) {
 | 
						|
            $i++;
 | 
						|
          }
 | 
						|
          $param = "$key$i";
 | 
						|
        }
 | 
						|
        # value ou [operator, value]
 | 
						|
        if (is_array($cond)) {
 | 
						|
          $op = null;
 | 
						|
          $value = null;
 | 
						|
          $condkeys = array_keys($cond);
 | 
						|
          if (array_key_exists("op", $cond)) $op = $cond["op"];
 | 
						|
          if (array_key_exists("value", $cond)) $value = $cond["value"];
 | 
						|
          $condkey = 0;
 | 
						|
          if ($op === null && array_key_exists($condkey, $condkeys)) {
 | 
						|
            $op = $cond[$condkeys[$condkey]];
 | 
						|
            $condkey++;
 | 
						|
          }
 | 
						|
          if ($value === null && array_key_exists($condkey, $condkeys)) {
 | 
						|
            $value = $cond[$condkeys[$condkey]];
 | 
						|
            $condkey++;
 | 
						|
          }
 | 
						|
        } else {
 | 
						|
          $op = "=";
 | 
						|
          $value = $cond;
 | 
						|
        }
 | 
						|
        $cond = [$key, $op];
 | 
						|
        if ($value !== null) {
 | 
						|
          $cond[] = ":$param";
 | 
						|
          $params[$param] = $value;
 | 
						|
        }
 | 
						|
        $condsql[] = implode(" ", $cond);
 | 
						|
      }
 | 
						|
    }
 | 
						|
    if ($sep === null) $sep = "and";
 | 
						|
    $count = count($condsql);
 | 
						|
    if ($count > 1) {
 | 
						|
      $sql[] = "(" . implode(" $sep ", $condsql) . ")";
 | 
						|
    } elseif ($count == 1) {
 | 
						|
      $sql[] = $condsql[0];
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  static function parse_set_values(?array $values, ?array &$sql, ?array &$params): void {
 | 
						|
    if (!$values) return;
 | 
						|
    $index = 0;
 | 
						|
    $parts = [];
 | 
						|
    foreach ($values as $key => $part) {
 | 
						|
      if ($key === $index) {
 | 
						|
        ## séquentiel
 | 
						|
        if (is_array($part)) {
 | 
						|
          # paramètres récursifs
 | 
						|
          self::parse_set_values($part, $parts, $params);
 | 
						|
        } else {
 | 
						|
          # paramètre litéral
 | 
						|
          $parts[] = strval($part);
 | 
						|
        }
 | 
						|
        $index++;
 | 
						|
      } else {
 | 
						|
        ## associatif
 | 
						|
        # paramètre
 | 
						|
        $param = $key;
 | 
						|
        if ($params !== null && array_key_exists($param, $params)) {
 | 
						|
          $i = 1;
 | 
						|
          while (array_key_exists("$key$i", $params)) {
 | 
						|
            $i++;
 | 
						|
          }
 | 
						|
          $param = "$key$i";
 | 
						|
        }
 | 
						|
        # value
 | 
						|
        $value = $part;
 | 
						|
        $part = [$key, "="];
 | 
						|
        if ($value === null) {
 | 
						|
          $part[] = "null";
 | 
						|
        } else {
 | 
						|
          $part[] = ":$param";
 | 
						|
          $params[$param] = $value;
 | 
						|
        }
 | 
						|
        $parts[] = implode(" ", $part);
 | 
						|
      }
 | 
						|
    }
 | 
						|
    $sql = cl::merge($sql, $parts);
 | 
						|
  }
 | 
						|
 | 
						|
  const create_SCHEMA = [
 | 
						|
    "prefix" => "string",
 | 
						|
    "table" => "string",
 | 
						|
    "schema" => "?array",
 | 
						|
    "cols" => "?array",
 | 
						|
    "suffix" => "?string",
 | 
						|
  ];
 | 
						|
  static function is_create(string $sql): bool {
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
  static function parse_create(array $query, ?array &$params=null): string {
 | 
						|
  }
 | 
						|
 | 
						|
  const select_SCHEMA = [
 | 
						|
    "prefix" => "string",
 | 
						|
    "schema" => "?array",
 | 
						|
    "cols" => "?array",
 | 
						|
    "from" => "?string",
 | 
						|
    "where" => "?array",
 | 
						|
    "order by" => "?array",
 | 
						|
    "group by" => "?array",
 | 
						|
    "having" => "?array",
 | 
						|
  ];
 | 
						|
  static function is_select(string $sql): bool {
 | 
						|
    return preg_match("/^select\b/i", $sql);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * parser une chaine de la forme
 | 
						|
   * "select [COLS] [from TABLE] [where CONDS] [order by ORDERS] [group by GROUPS] [having CONDS]"
 | 
						|
   */
 | 
						|
  static function parse_select(array $query, ?array &$params=null): string {
 | 
						|
    # fusionner d'abord toutes les parties séquentielles
 | 
						|
    $usersql = $tmpsql = self::merge_seq($query);
 | 
						|
    ### vérifier la présence des parties nécessaires
 | 
						|
    $sql = [];
 | 
						|
    if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix;
 | 
						|
 | 
						|
    ## select
 | 
						|
    self::consume('select\s*', $tmpsql); $sql[] = "select";
 | 
						|
 | 
						|
    ## cols
 | 
						|
    $usercols = [];
 | 
						|
    if (self::consume('(.*?)\s*(?=$|\bfrom\b)', $tmpsql, $ms)) {
 | 
						|
      if ($ms[1]) $usercols[] = $ms[1];
 | 
						|
    }
 | 
						|
    $tmpcols = cl::withn($query["cols"] ?? null);
 | 
						|
    $schema = $query["schema"] ?? null;
 | 
						|
    if ($tmpcols !== null) {
 | 
						|
      $cols = [];
 | 
						|
      $index = 0;
 | 
						|
      foreach ($tmpcols as $key => $col) {
 | 
						|
        if ($key === $index) {
 | 
						|
          $index++;
 | 
						|
          $cols[] = $col;
 | 
						|
          $usercols[] = $col;
 | 
						|
        } else {
 | 
						|
          $cols[] = $key;
 | 
						|
          $usercols[] = "$col as $key";
 | 
						|
        }
 | 
						|
      }
 | 
						|
    } else {
 | 
						|
      $cols = null;
 | 
						|
      if ($schema && is_array($schema) && !in_array("*", $usercols)) {
 | 
						|
        $cols = array_keys($schema);
 | 
						|
        $usercols = array_merge($usercols, $cols);
 | 
						|
      }
 | 
						|
    }
 | 
						|
    if (!$usercols && !$cols) $usercols = ["*"];
 | 
						|
    $sql[] = implode(" ", $usercols);
 | 
						|
 | 
						|
    ## from
 | 
						|
    $from = $query["from"] ?? null;
 | 
						|
    if (self::consume('from\s+([a-z_][a-z0-9_]*)\s*(?=;?\s*$|\bwhere\b)', $tmpsql, $ms)) {
 | 
						|
      if ($from === null) $from = $ms[1];
 | 
						|
      $sql[] = "from";
 | 
						|
      $sql[] = $from;
 | 
						|
    } elseif ($from !== null) {
 | 
						|
      $sql[] = "from";
 | 
						|
      $sql[] = $from;
 | 
						|
    } else {
 | 
						|
      throw new ValueException("expected table name: $usersql");
 | 
						|
    }
 | 
						|
 | 
						|
    ## where
 | 
						|
    $userwhere = [];
 | 
						|
    if (self::consume('where\b\s*(.*?)(?=;?\s*$|\border\s+by\b)', $tmpsql, $ms)) {
 | 
						|
      if ($ms[1]) $userwhere[] = $ms[1];
 | 
						|
    }
 | 
						|
    $where = cl::withn($query["where"] ?? null);
 | 
						|
    if ($where !== null) self::parse_conds($where, $userwhere, $params);
 | 
						|
    if ($userwhere) {
 | 
						|
      $sql[] = "where";
 | 
						|
      $sql[] = implode(" and ", $userwhere);
 | 
						|
    }
 | 
						|
 | 
						|
    ## order by
 | 
						|
    $userorderby = [];
 | 
						|
    if (self::consume('order\s+by\b\s*(.*?)(?=;?\s*$|\bgroup\s+by\b)', $tmpsql, $ms)) {
 | 
						|
      if ($ms[1]) $userorderby[] = $ms[1];
 | 
						|
    }
 | 
						|
    $orderby = cl::withn($query["order by"] ?? null);
 | 
						|
    if ($orderby !== null) {
 | 
						|
      $index = 0;
 | 
						|
      foreach ($orderby as $key => $value) {
 | 
						|
        if ($key === $index) {
 | 
						|
          $userorderby[] = $value;
 | 
						|
          $index++;
 | 
						|
        } elseif ($value !== null) {
 | 
						|
          if (is_bool($value)) $value = $value? "asc": "desc";
 | 
						|
          $userorderby[] = "$key $value";
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
    if ($userorderby) {
 | 
						|
      $sql[] = "order by";
 | 
						|
      $sql[] = implode(", ", $userorderby);
 | 
						|
    }
 | 
						|
    ## group by
 | 
						|
    $usergroupby = [];
 | 
						|
    if (self::consume('group\s+by\b\s*(.*?)(?=;?\s*$|\bhaving\b)', $tmpsql, $ms)) {
 | 
						|
      if ($ms[1]) $usergroupby[] = $ms[1];
 | 
						|
    }
 | 
						|
    $groupby = cl::withn($query["group by"] ?? null);
 | 
						|
    if ($groupby !== null) {
 | 
						|
      $index = 0;
 | 
						|
      foreach ($groupby as $key => $value) {
 | 
						|
        if ($key === $index) {
 | 
						|
          $usergroupby[] = $value;
 | 
						|
          $index++;
 | 
						|
        } elseif ($value !== null) {
 | 
						|
          $usergroupby[] = $key;
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
    if ($usergroupby) {
 | 
						|
      $sql[] = "group by";
 | 
						|
      $sql[] = implode(", ", $usergroupby);
 | 
						|
    }
 | 
						|
 | 
						|
    ## having
 | 
						|
    $userhaving = [];
 | 
						|
    if (self::consume('having\b\s*(.*?)(?=;?\s*$)', $tmpsql, $ms)) {
 | 
						|
      if ($ms[1]) $userhaving[] = $ms[1];
 | 
						|
    }
 | 
						|
    $having = cl::withn($query["having"] ?? null);
 | 
						|
    if ($having !== null) self::parse_conds($having, $userhaving, $params);
 | 
						|
    if ($userhaving) {
 | 
						|
      $sql[] = "having";
 | 
						|
      $sql[] = implode(" and ", $userhaving);
 | 
						|
    }
 | 
						|
 | 
						|
    ## fin de la requête
 | 
						|
    self::consume(';\s*', $tmpsql);
 | 
						|
    if ($tmpsql) {
 | 
						|
      throw new ValueException("unexpected value at end: $usersql");
 | 
						|
    }
 | 
						|
    return implode(" ", $sql);
 | 
						|
  }
 | 
						|
 | 
						|
  const insert_SCHEMA = [
 | 
						|
    "prefix" => "string",
 | 
						|
    "into" => "?string",
 | 
						|
    "schema" => "?array",
 | 
						|
    "cols" => "?array",
 | 
						|
    "values" => "?array",
 | 
						|
  ];
 | 
						|
  static function is_insert(string $sql): bool {
 | 
						|
    return preg_match("/^insert\b/i", $sql);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * parser une chaine de la forme
 | 
						|
   * "insert [into] [TABLE] [(COLS)] [values (VALUES)]"
 | 
						|
   */
 | 
						|
  static function parse_insert(array $query, ?array &$params=null): string {
 | 
						|
    # fusionner d'abord toutes les parties séquentielles
 | 
						|
    $usersql = $tmpsql = self::merge_seq($query);
 | 
						|
    # vérifier la présence des parties nécessaires
 | 
						|
    $sql = [];
 | 
						|
    if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix;
 | 
						|
    self::consume('insert\s*', $tmpsql); $sql[] = "insert";
 | 
						|
    self::consume('into\s*', $tmpsql); $sql[] = "into";
 | 
						|
    $into = $query["into"] ?? null;
 | 
						|
    if (self::consume('([a-z_][a-z0-9_]*)\s*', $tmpsql, $ms)) {
 | 
						|
      if ($into === null) $into = $ms[1];
 | 
						|
      $sql[] = $into;
 | 
						|
    } elseif ($into !== null) {
 | 
						|
      $sql[] = $into;
 | 
						|
    } else {
 | 
						|
      throw new ValueException("expected table name: $usersql");
 | 
						|
    }
 | 
						|
    $usercols = [];
 | 
						|
    $uservalues = [];
 | 
						|
    if (self::consume('\(([^)]*)\)\s*', $tmpsql, $ms)) {
 | 
						|
      $usercols = array_merge($usercols, preg_split("/\s*,\s*/", $ms[1]));
 | 
						|
    }
 | 
						|
    $cols = cl::withn($query["cols"] ?? null);
 | 
						|
    $values = cl::withn($query["values"] ?? null);
 | 
						|
    $schema = $query["schema"] ?? null;
 | 
						|
    if ($cols === null) {
 | 
						|
      if ($usercols) {
 | 
						|
        $cols = $usercols;
 | 
						|
      } elseif ($values) {
 | 
						|
        $cols = array_keys($values);
 | 
						|
        $usercols = array_merge($usercols, $cols);
 | 
						|
      } elseif ($schema && is_array($schema)) {
 | 
						|
        #XXX implémenter support AssocSchema
 | 
						|
        $cols = array_keys($schema);
 | 
						|
        $usercols = array_merge($usercols, $cols);
 | 
						|
      }
 | 
						|
    }
 | 
						|
    if (self::consume('values\s+\(\s*(.*)\s*\)\s*', $tmpsql, $ms)) {
 | 
						|
      if ($ms[1]) $uservalues[] = $ms[1];
 | 
						|
    }
 | 
						|
    self::consume(';\s*', $tmpsql);
 | 
						|
    if ($tmpsql) {
 | 
						|
      throw new ValueException("unexpected value at end: $usersql");
 | 
						|
    }
 | 
						|
    if ($cols !== null && !$uservalues) {
 | 
						|
      if (!$usercols) $usercols = $cols;
 | 
						|
      foreach ($cols as $col) {
 | 
						|
        $uservalues[] = ":$col";
 | 
						|
        $params[$col] = $values[$col] ?? null;
 | 
						|
      }
 | 
						|
    }
 | 
						|
    $sql[] = "(".implode(", ", $usercols).")";
 | 
						|
    $sql[] = "values (".implode(", ", $uservalues).")";
 | 
						|
    return implode(" ", $sql);
 | 
						|
  }
 | 
						|
 | 
						|
  const update_SCHEMA = [
 | 
						|
    "prefix" => "string",
 | 
						|
    "table" => "?string",
 | 
						|
    "schema" => "?array",
 | 
						|
    "cols" => "?array",
 | 
						|
    "values" => "?array",
 | 
						|
    "where" => "?array",
 | 
						|
  ];
 | 
						|
  static function is_update(string $sql): bool {
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
  static function parse_update(array $query, ?array &$params=null): string {
 | 
						|
  }
 | 
						|
 | 
						|
  const delete_SCHEMA = [
 | 
						|
    "prefix" => "string",
 | 
						|
    "from" => "?string",
 | 
						|
    "where" => "?array",
 | 
						|
  ];
 | 
						|
  static function is_delete(string $sql): bool {
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
  static function parse_delete(array $query, ?array &$params=null): string {
 | 
						|
  }
 | 
						|
 | 
						|
  function __construct($sql, ?array $params=null) {
 | 
						|
    self::verifix($sql, $params);
 | 
						|
    $this->sql = $sql;
 | 
						|
    $this->params = $params;
 | 
						|
  }
 | 
						|
 | 
						|
  /** @var string */
 | 
						|
  protected $sql;
 | 
						|
 | 
						|
  /** @var ?array */
 | 
						|
  protected $params;
 | 
						|
 | 
						|
  function useStmt(SQLite3 $db, ?SQLite3Stmt &$stmt=null, ?string &$sql=null): bool {
 | 
						|
    if ($this->params !== null) {
 | 
						|
      /** @var SQLite3Stmt $stmt */
 | 
						|
      $stmt = SqliteException::check($db, $db->prepare($this->sql));
 | 
						|
      $close = true;
 | 
						|
      try {
 | 
						|
        foreach ($this->params as $param => $value) {
 | 
						|
          SqliteException::check($db, $stmt->bindValue($param, $value));
 | 
						|
        }
 | 
						|
        $close = false;
 | 
						|
        return true;
 | 
						|
      } finally {
 | 
						|
        if ($close) $stmt->close();
 | 
						|
      }
 | 
						|
    } else {
 | 
						|
      $sql = $this->sql;
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 |