From bd1f901b70943e9e03ed56e15ebd4dbaece61b00 Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Wed, 9 Apr 2025 23:18:06 +0400 Subject: [PATCH] =?UTF-8?q?r=C3=A9organiser=20le=20code=20de=20g=C3=A9n?= =?UTF-8?q?=C3=A9ration=20sql?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- composer.json | 1 + php/src/db/_private/Tcreate.php | 39 --- php/src/db/_private/Tdelete.php | 38 --- php/src/db/_private/Tgeneric.php | 18 -- php/src/db/_private/Tinsert.php | 82 ------ php/src/db/_private/Tselect.php | 168 ------------ php/src/db/_private/Tupdate.php | 40 --- php/src/db/_private/_base.php | 252 +---------------- php/src/db/_private/_common.php | 255 ++++++++++++++++++ php/src/db/_private/_create.php | 37 ++- php/src/db/_private/_delete.php | 35 ++- php/src/db/_private/_generic.php | 16 +- php/src/db/_private/_insert.php | 80 +++++- php/src/db/_private/_select.php | 166 +++++++++++- php/src/db/_private/_update.php | 38 ++- php/src/db/mysql/MysqlStorage.php | 2 +- php/src/db/mysql/_query_base.php | 52 ---- php/src/db/mysql/_query_create.php | 10 - php/src/db/mysql/_query_delete.php | 10 - php/src/db/mysql/_query_generic.php | 10 - php/src/db/mysql/_query_insert.php | 10 - php/src/db/mysql/_query_select.php | 10 - php/src/db/mysql/_query_update.php | 10 - php/src/db/mysql/query.php | 9 +- php/src/db/pdo/Pdo.php | 6 +- php/src/db/pdo/_query_create.php | 10 - php/src/db/pdo/_query_delete.php | 10 - php/src/db/pdo/_query_generic.php | 10 - php/src/db/pdo/_query_insert.php | 10 - php/src/db/pdo/_query_select.php | 10 - php/src/db/pdo/_query_update.php | 10 - php/src/db/pdo/{_query_base.php => query.php} | 46 ++-- php/src/db/sqlite/Sqlite.php | 6 +- php/src/db/sqlite/SqliteStorage.php | 2 +- php/src/db/sqlite/_query_create.php | 10 - php/src/db/sqlite/_query_delete.php | 10 - php/src/db/sqlite/_query_generic.php | 10 - php/src/db/sqlite/_query_insert.php | 10 - php/src/db/sqlite/_query_select.php | 10 - php/src/db/sqlite/_query_update.php | 10 - .../db/sqlite/{_query_base.php => query.php} | 34 ++- php/tests/db/sqlite/_queryTest.php | 46 ++-- 42 files changed, 705 insertions(+), 943 deletions(-) delete mode 100644 php/src/db/_private/Tcreate.php delete mode 100644 php/src/db/_private/Tdelete.php delete mode 100644 php/src/db/_private/Tgeneric.php delete mode 100644 php/src/db/_private/Tinsert.php delete mode 100644 php/src/db/_private/Tselect.php delete mode 100644 php/src/db/_private/Tupdate.php create mode 100644 php/src/db/_private/_common.php delete mode 100644 php/src/db/mysql/_query_base.php delete mode 100644 php/src/db/mysql/_query_create.php delete mode 100644 php/src/db/mysql/_query_delete.php delete mode 100644 php/src/db/mysql/_query_generic.php delete mode 100644 php/src/db/mysql/_query_insert.php delete mode 100644 php/src/db/mysql/_query_select.php delete mode 100644 php/src/db/mysql/_query_update.php delete mode 100644 php/src/db/pdo/_query_create.php delete mode 100644 php/src/db/pdo/_query_delete.php delete mode 100644 php/src/db/pdo/_query_generic.php delete mode 100644 php/src/db/pdo/_query_insert.php delete mode 100644 php/src/db/pdo/_query_select.php delete mode 100644 php/src/db/pdo/_query_update.php rename php/src/db/pdo/{_query_base.php => query.php} (62%) delete mode 100644 php/src/db/sqlite/_query_create.php delete mode 100644 php/src/db/sqlite/_query_delete.php delete mode 100644 php/src/db/sqlite/_query_generic.php delete mode 100644 php/src/db/sqlite/_query_insert.php delete mode 100644 php/src/db/sqlite/_query_select.php delete mode 100644 php/src/db/sqlite/_query_update.php rename php/src/db/sqlite/{_query_base.php => query.php} (60%) diff --git a/composer.json b/composer.json index 7171bf4..346a248 100644 --- a/composer.json +++ b/composer.json @@ -24,6 +24,7 @@ "ext-posix": "*", "ext-pcntl": "*", "ext-curl": "*", + "ext-pdo": "*", "ext-sqlite3": "*" }, "autoload": { diff --git a/php/src/db/_private/Tcreate.php b/php/src/db/_private/Tcreate.php deleted file mode 100644 index a85d118..0000000 --- a/php/src/db/_private/Tcreate.php +++ /dev/null @@ -1,39 +0,0 @@ - &$definition) { - if ($col === $index) { - $index++; - } else { - $definition = "$col $definition"; - } - }; unset($definition); - $sql[] = "(\n ".implode("\n, ", $cols)."\n)"; - - ## suffixe - if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix; - - ## fin de la requête - return implode(" ", $sql); - } -} diff --git a/php/src/db/_private/Tdelete.php b/php/src/db/_private/Tdelete.php deleted file mode 100644 index 0eeb91a..0000000 --- a/php/src/db/_private/Tdelete.php +++ /dev/null @@ -1,38 +0,0 @@ - $col) { - if ($key === $index) { - $index++; - $cols[] = $col; - $usercols[] = self::add_prefix($col, $colPrefix); - } else { - $cols[] = $key; - $usercols[] = self::add_prefix($col, $colPrefix)." as $key"; - } - } - } else { - $cols = null; - if ($schema && is_array($schema) && !in_array("*", $usercols)) { - $cols = array_keys($schema); - foreach ($cols as $col) { - $usercols[] = self::add_prefix($col, $colPrefix); - } - } - } - if (!$usercols && !$cols) $usercols = [self::add_prefix("*", $colPrefix)]; - $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, $bindings); - 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++; - } else { - if ($value === null) $value = false; - if (!is_bool($value)) { - $userorderby[] = "$key $value"; - } elseif ($value) { - $userorderby[] = $key; - } - } - } - } - 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++; - } else { - if ($value === null) $value = false; - if (!is_bool($value)) { - $usergroupby[] = "$key $value"; - } elseif ($value) { - $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, $bindings); - if ($userhaving) { - $sql[] = "having"; - $sql[] = implode(" and ", $userhaving); - } - - ## suffixe - if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix; - - ## fin de la requête - self::check_eof($tmpsql, $usersql); - return implode(" ", $sql); - } -} diff --git a/php/src/db/_private/Tupdate.php b/php/src/db/_private/Tupdate.php deleted file mode 100644 index 4e1de5b..0000000 --- a/php/src/db/_private/Tupdate.php +++ /dev/null @@ -1,40 +0,0 @@ - $value) { - if ($key === $index) { - $index++; - if ($sql && !str::ends_with(" ", $sql) && !str::starts_with(" ", $value)) { - $sql .= " "; - } - $sql .= $value; - } - } - return $sql; + static function with($sql, ?array $params=null): array { + static::verifix($sql, $params); + return [$sql, $params]; } - protected 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 &$bindings): 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_bool($cond)) { - # ignorer les valeurs true et false - } elseif (is_array($cond)) { - # condition récursive - self::parse_conds($cond, $condsql, $bindings); - } else { - # condition litérale - $condsql[] = strval($cond); - } - $index++; - } elseif ($cond === false) { - ## associatif - # condition litérale ignorée car condition false - } elseif ($cond === true) { - # condition litérale sélectionnée car condition true - $condsql[] = strval($key); - } else { - ## associatif - # paramètre - $param0 = preg_replace('/^.+\./', "", $key); - $i = false; - if ($bindings !== null && array_key_exists($param0, $bindings)) { - $i = 2; - while (array_key_exists("$param0$i", $bindings)) { - $i++; - } - } - # value ou [operator, value] - $condprefix = $condsep = $condsuffix = null; - if (is_array($cond)) { - $condkey = 0; - $condkeys = array_keys($cond); - $op = null; - if (array_key_exists("op", $cond)) { - $op = $cond["op"]; - } elseif (array_key_exists($condkey, $condkeys)) { - $op = $cond[$condkeys[$condkey]]; - $condkey++; - } - $op = strtolower($op); - $condvalues = null; - switch ($op) { - case "between": - # ["between", $upper, $lower] - $condsep = " and "; - if (array_key_exists("lower", $cond)) { - $condvalues[] = $cond["lower"]; - } elseif (array_key_exists($condkey, $condkeys)) { - $condvalues[] = $cond[$condkeys[$condkey]]; - $condkey++; - } - if (array_key_exists("upper", $cond)) { - $condvalues[] = $cond["upper"]; - } elseif (array_key_exists($condkey, $condkeys)) { - $condvalues[] = $cond[$condkeys[$condkey]]; - $condkey++; - } - break; - case "any": - case "all": - case "not any": - case "not all": - # ["list", $values] - if ($op === "any" || $op === "all") { - $condprefix = $op; - $op = "="; - } elseif ($op === "not any" || $op === "not all") { - $condprefix = substr($op, strlen("not ")); - $op = "<>"; - } - $condprefix .= "(array["; - $condsep = ", "; - $condsuffix = "])"; - $condvalues = null; - if (array_key_exists("values", $cond)) { - $condvalues = cl::with($cond["values"]); - } elseif (array_key_exists($condkey, $condkeys)) { - $condvalues = cl::with($cond[$condkeys[$condkey]]); - $condkey++; - } - break; - case "in": - # ["in", $values] - $condprefix = "("; - $condsep = ", "; - $condsuffix = ")"; - $condvalues = null; - if (array_key_exists("values", $cond)) { - $condvalues = cl::with($cond["values"]); - } elseif (array_key_exists($condkey, $condkeys)) { - $condvalues = cl::with($cond[$condkeys[$condkey]]); - $condkey++; - } - break; - case "null": - case "is null": - $op = "is null"; - break; - case "not null": - case "is not null": - $op = "is not null"; - break; - default: - if (array_key_exists("value", $cond)) { - $condvalues = [$cond["value"]]; - } elseif (array_key_exists($condkey, $condkeys)) { - $condvalues = [$cond[$condkeys[$condkey]]]; - $condkey++; - } - } - } elseif ($cond !== null) { - $op = "="; - $condvalues = [$cond]; - } else { - $op = "is null"; - $condvalues = null; - } - $cond = [$key, $op]; - if ($condvalues !== null) { - $parts = []; - foreach ($condvalues as $condvalue) { - if (is_array($condvalue)) { - $first = true; - foreach ($condvalue as $value) { - if ($first) { - $first = false; - } else { - if ($sep === null) $sep = "and"; - $parts[] = " $sep "; - $parts[] = $key; - $parts[] = " $op "; - } - $param = "$param0$i"; - $parts[] = ":$param"; - $bindings[$param] = $value; - if ($i === false) $i = 2; - else $i++; - } - } else { - $param = "$param0$i"; - $parts[] = ":$param"; - $bindings[$param] = $condvalue; - if ($i === false) $i = 2; - else $i++; - } - } - $cond[] = $condprefix.implode($condsep, $parts).$condsuffix; - } - $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 &$bindings): 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, $bindings); - } else { - # paramètre litéral - $parts[] = strval($part); - } - $index++; - } else { - ## associatif - # paramètre - $param = $param0 = preg_replace('/^.+\./', "", $key); - if ($bindings !== null && array_key_exists($param0, $bindings)) { - $i = 2; - while (array_key_exists("$param0$i", $bindings)) { - $i++; - } - $param = "$param0$i"; - } - # value - $value = $part; - $part = [$key, "="]; - if ($value === null) { - $part[] = "null"; - } else { - $part[] = ":$param"; - $bindings[$param] = $value; - } - $parts[] = implode(" ", $part); - } - } - $sql = cl::merge($sql, $parts); - } - - protected static function check_eof(string $tmpsql, string $usersql): void { - self::consume(';\s*', $tmpsql); - if ($tmpsql) { - throw new ValueException("unexpected value at end: $usersql"); - } - } - - abstract protected static function verifix(&$sql, ?array &$bindinds=null, ?array &$meta=null): void; - function __construct($sql, ?array $bindings=null) { static::verifix($sql, $bindings, $meta); $this->sql = $sql; diff --git a/php/src/db/_private/_common.php b/php/src/db/_private/_common.php new file mode 100644 index 0000000..575a53b --- /dev/null +++ b/php/src/db/_private/_common.php @@ -0,0 +1,255 @@ + $value) { + if ($key === $index) { + $index++; + if ($sql && !str::ends_with(" ", $sql) && !str::starts_with(" ", $value)) { + $sql .= " "; + } + $sql .= $value; + } + } + return $sql; + } + + protected 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 &$bindings): 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_bool($cond)) { + # ignorer les valeurs true et false + } elseif (is_array($cond)) { + # condition récursive + self::parse_conds($cond, $condsql, $bindings); + } else { + # condition litérale + $condsql[] = strval($cond); + } + $index++; + } elseif ($cond === false) { + ## associatif + # condition litérale ignorée car condition false + } elseif ($cond === true) { + # condition litérale sélectionnée car condition true + $condsql[] = strval($key); + } else { + ## associatif + # paramètre + $param0 = preg_replace('/^.+\./', "", $key); + $i = false; + if ($bindings !== null && array_key_exists($param0, $bindings)) { + $i = 2; + while (array_key_exists("$param0$i", $bindings)) { + $i++; + } + } + # value ou [operator, value] + $condprefix = $condsep = $condsuffix = null; + if (is_array($cond)) { + $condkey = 0; + $condkeys = array_keys($cond); + $op = null; + if (array_key_exists("op", $cond)) { + $op = $cond["op"]; + } elseif (array_key_exists($condkey, $condkeys)) { + $op = $cond[$condkeys[$condkey]]; + $condkey++; + } + $op = strtolower($op); + $condvalues = null; + switch ($op) { + case "between": + # ["between", $upper, $lower] + $condsep = " and "; + if (array_key_exists("lower", $cond)) { + $condvalues[] = $cond["lower"]; + } elseif (array_key_exists($condkey, $condkeys)) { + $condvalues[] = $cond[$condkeys[$condkey]]; + $condkey++; + } + if (array_key_exists("upper", $cond)) { + $condvalues[] = $cond["upper"]; + } elseif (array_key_exists($condkey, $condkeys)) { + $condvalues[] = $cond[$condkeys[$condkey]]; + $condkey++; + } + break; + case "any": + case "all": + case "not any": + case "not all": + # ["list", $values] + if ($op === "any" || $op === "all") { + $condprefix = $op; + $op = "="; + } elseif ($op === "not any" || $op === "not all") { + $condprefix = substr($op, strlen("not ")); + $op = "<>"; + } + $condprefix .= "(array["; + $condsep = ", "; + $condsuffix = "])"; + $condvalues = null; + if (array_key_exists("values", $cond)) { + $condvalues = cl::with($cond["values"]); + } elseif (array_key_exists($condkey, $condkeys)) { + $condvalues = cl::with($cond[$condkeys[$condkey]]); + $condkey++; + } + break; + case "in": + # ["in", $values] + $condprefix = "("; + $condsep = ", "; + $condsuffix = ")"; + $condvalues = null; + if (array_key_exists("values", $cond)) { + $condvalues = cl::with($cond["values"]); + } elseif (array_key_exists($condkey, $condkeys)) { + $condvalues = cl::with($cond[$condkeys[$condkey]]); + $condkey++; + } + break; + case "null": + case "is null": + $op = "is null"; + break; + case "not null": + case "is not null": + $op = "is not null"; + break; + default: + if (array_key_exists("value", $cond)) { + $condvalues = [$cond["value"]]; + } elseif (array_key_exists($condkey, $condkeys)) { + $condvalues = [$cond[$condkeys[$condkey]]]; + $condkey++; + } + } + } elseif ($cond !== null) { + $op = "="; + $condvalues = [$cond]; + } else { + $op = "is null"; + $condvalues = null; + } + $cond = [$key, $op]; + if ($condvalues !== null) { + $parts = []; + foreach ($condvalues as $condvalue) { + if (is_array($condvalue)) { + $first = true; + foreach ($condvalue as $value) { + if ($first) { + $first = false; + } else { + if ($sep === null) $sep = "and"; + $parts[] = " $sep "; + $parts[] = $key; + $parts[] = " $op "; + } + $param = "$param0$i"; + $parts[] = ":$param"; + $bindings[$param] = $value; + if ($i === false) $i = 2; + else $i++; + } + } else { + $param = "$param0$i"; + $parts[] = ":$param"; + $bindings[$param] = $condvalue; + if ($i === false) $i = 2; + else $i++; + } + } + $cond[] = $condprefix.implode($condsep, $parts).$condsuffix; + } + $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 &$bindings): 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, $bindings); + } else { + # paramètre litéral + $parts[] = strval($part); + } + $index++; + } else { + ## associatif + # paramètre + $param = $param0 = preg_replace('/^.+\./', "", $key); + if ($bindings !== null && array_key_exists($param0, $bindings)) { + $i = 2; + while (array_key_exists("$param0$i", $bindings)) { + $i++; + } + $param = "$param0$i"; + } + # value + $value = $part; + $part = [$key, "="]; + if ($value === null) { + $part[] = "null"; + } else { + $part[] = ":$param"; + $bindings[$param] = $value; + } + $parts[] = implode(" ", $part); + } + } + $sql = cl::merge($sql, $parts); + } + + protected static function check_eof(string $tmpsql, string $usersql): void { + self::consume(';\s*', $tmpsql); + if ($tmpsql) { + throw new ValueException("unexpected value at end: $usersql"); + } + } +} diff --git a/php/src/db/_private/_create.php b/php/src/db/_private/_create.php index 64c29a5..030c594 100644 --- a/php/src/db/_private/_create.php +++ b/php/src/db/_private/_create.php @@ -1,7 +1,7 @@ "?string", "table" => "string", @@ -9,4 +9,39 @@ class _create { "cols" => "?array", "suffix" => "?string", ]; + + static function isa(string $sql): bool { + //return preg_match("/^create(?:\s+table)?\b/i", $sql); + #XXX implémentation minimale + return preg_match("/^create\s+table\b/i", $sql); + } + + static function parse(array $query, ?array &$bindings=null): string { + #XXX implémentation minimale + $sql = [self::merge_seq($query)]; + + ## préfixe + if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix; + + ## table + $sql[] = $query["table"]; + + ## columns + $cols = $query["cols"]; + $index = 0; + foreach ($cols as $col => &$definition) { + if ($col === $index) { + $index++; + } else { + $definition = "$col $definition"; + } + }; unset($definition); + $sql[] = "(\n ".implode("\n, ", $cols)."\n)"; + + ## suffixe + if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix; + + ## fin de la requête + return implode(" ", $sql); + } } diff --git a/php/src/db/_private/_delete.php b/php/src/db/_private/_delete.php index e79ec34..fd78dbb 100644 --- a/php/src/db/_private/_delete.php +++ b/php/src/db/_private/_delete.php @@ -1,11 +1,44 @@ "?string", "from" => "?string", "where" => "?array", "suffix" => "?string", ]; + + static function isa(string $sql): bool { + return preg_match("/^delete(?:\s+from)?\b/i", $sql); + } + + static function parse(array $query, ?array &$bindings=null): string { + #XXX implémentation minimale + $tmpsql = self::merge_seq($query); + self::consume('delete(?:\s+from)?\b', $tmpsql); + $sql = ["delete from", $tmpsql]; + + ## préfixe + if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix; + + ## table + $sql[] = $query["from"]; + + ## where + $where = $query["where"] ?? null; + if ($where !== null) { + self::parse_conds($where, $wheresql, $bindings); + if ($wheresql) { + $sql[] = "where"; + $sql[] = implode(" and ", $wheresql); + } + } + + ## suffixe + if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix; + + ## fin de la requête + return implode(" ", $sql); + } } diff --git a/php/src/db/_private/_generic.php b/php/src/db/_private/_generic.php index 97d4b51..1fef213 100644 --- a/php/src/db/_private/_generic.php +++ b/php/src/db/_private/_generic.php @@ -1,7 +1,21 @@ "?string", "into" => "?string", @@ -10,4 +13,79 @@ class _insert { "values" => "?array", "suffix" => "?string", ]; + + static function isa(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(array $query, ?array &$bindings=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; + + ## insert + self::consume('(insert(?:\s+or\s+(?:ignore|replace))?)\s*', $tmpsql, $ms); + $sql[] = $ms[1]; + + ## into + 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"); + } + + ## cols & values + $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]; + } + if ($cols !== null && !$uservalues) { + if (!$usercols) $usercols = $cols; + foreach ($cols as $col) { + $uservalues[] = ":$col"; + $bindings[$col] = $values[$col] ?? null; + } + } + $sql[] = "(" . implode(", ", $usercols) . ")"; + $sql[] = "values (" . implode(", ", $uservalues) . ")"; + + ## suffixe + if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix; + + ## fin de la requête + self::check_eof($tmpsql, $usersql); + return implode(" ", $sql); + } } diff --git a/php/src/db/_private/_select.php b/php/src/db/_private/_select.php index ee2bdbc..ea343f1 100644 --- a/php/src/db/_private/_select.php +++ b/php/src/db/_private/_select.php @@ -1,7 +1,11 @@ "?string", "schema" => "?array", @@ -14,4 +18,164 @@ class _select { "having" => "?array", "suffix" => "?string", ]; + + static function isa(string $sql): bool { + return preg_match("/^select\b/i", $sql); + } + + private static function add_prefix(string $col, ?string $prefix): string { + if ($prefix === null) return $col; + if (strpos($col, ".") !== false) return $col; + return "$prefix$col"; + } + + /** + * parser une chaine de la forme + * "select [COLS] [from TABLE] [where CONDS] [order by ORDERS] [group by GROUPS] [having CONDS]" + */ + static function parse(array $query, ?array &$bindings=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 = []; + + ## préfixe + if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix; + + ## select + self::consume('(select(?:\s*distinct)?)\s*', $tmpsql, $ms); + $sql[] = $ms[1]; + + ## cols + $usercols = []; + if (self::consume('(.*?)\s*(?=$|\bfrom\b)', $tmpsql, $ms)) { + if ($ms[1]) $usercols[] = $ms[1]; + } + $colPrefix = $query["col_prefix"] ?? null; + if ($colPrefix !== null) str::add_suffix($colPrefix, "."); + $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[] = self::add_prefix($col, $colPrefix); + } else { + $cols[] = $key; + $usercols[] = self::add_prefix($col, $colPrefix)." as $key"; + } + } + } else { + $cols = null; + if ($schema && is_array($schema) && !in_array("*", $usercols)) { + $cols = array_keys($schema); + foreach ($cols as $col) { + $usercols[] = self::add_prefix($col, $colPrefix); + } + } + } + if (!$usercols && !$cols) $usercols = [self::add_prefix("*", $colPrefix)]; + $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, $bindings); + 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++; + } else { + if ($value === null) $value = false; + if (!is_bool($value)) { + $userorderby[] = "$key $value"; + } elseif ($value) { + $userorderby[] = $key; + } + } + } + } + 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++; + } else { + if ($value === null) $value = false; + if (!is_bool($value)) { + $usergroupby[] = "$key $value"; + } elseif ($value) { + $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, $bindings); + if ($userhaving) { + $sql[] = "having"; + $sql[] = implode(" and ", $userhaving); + } + + ## suffixe + if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix; + + ## fin de la requête + self::check_eof($tmpsql, $usersql); + return implode(" ", $sql); + } } diff --git a/php/src/db/_private/_update.php b/php/src/db/_private/_update.php index b5b2dc6..97cf75d 100644 --- a/php/src/db/_private/_update.php +++ b/php/src/db/_private/_update.php @@ -1,7 +1,7 @@ "?string", "table" => "?string", @@ -11,4 +11,40 @@ class _update { "where" => "?array", "suffix" => "?string", ]; + + static function isa(string $sql): bool { + return preg_match("/^update\b/i", $sql); + } + + static function parse(array $query, ?array &$bindings=null): string { + #XXX implémentation minimale + $sql = [self::merge_seq($query)]; + + ## préfixe + if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix; + + ## table + $sql[] = $query["table"]; + + ## set + self::parse_set_values($query["values"], $setsql, $bindings); + $sql[] = "set"; + $sql[] = implode(", ", $setsql); + + ## where + $where = $query["where"] ?? null; + if ($where !== null) { + self::parse_conds($where, $wheresql, $bindings); + if ($wheresql) { + $sql[] = "where"; + $sql[] = implode(" and ", $wheresql); + } + } + + ## suffixe + if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix; + + ## fin de la requête + return implode(" ", $sql); + } } diff --git a/php/src/db/mysql/MysqlStorage.php b/php/src/db/mysql/MysqlStorage.php index 50b09d2..4d8cab5 100644 --- a/php/src/db/mysql/MysqlStorage.php +++ b/php/src/db/mysql/MysqlStorage.php @@ -24,7 +24,7 @@ class MysqlStorage extends CapacitorStorage { ]; function _getCreateSql(CapacitorChannel $channel): string { - $query = new _query_base($this->_createSql($channel)); + $query = new query($this->_createSql($channel)); return self::format_sql($channel, $query->getSql()); } diff --git a/php/src/db/mysql/_query_base.php b/php/src/db/mysql/_query_base.php deleted file mode 100644 index 614ec06..0000000 --- a/php/src/db/mysql/_query_base.php +++ /dev/null @@ -1,52 +0,0 @@ - "create", "type" => "ddl"]; - } elseif (_query_select::isa($prefix)) { - $sql = _query_select::parse($sql, $bindinds); - $meta = ["isa" => "select", "type" => "dql"]; - } elseif (_query_insert::isa($prefix)) { - $sql = _query_insert::parse($sql, $bindinds); - $meta = ["isa" => "insert", "type" => "dml"]; - } elseif (_query_update::isa($prefix)) { - $sql = _query_update::parse($sql, $bindinds); - $meta = ["isa" => "update", "type" => "dml"]; - } elseif (_query_delete::isa($prefix)) { - $sql = _query_delete::parse($sql, $bindinds); - $meta = ["isa" => "delete", "type" => "dml"]; - } elseif (_query_generic::isa($prefix)) { - $sql = _query_generic::parse($sql, $bindinds); - $meta = ["isa" => "generic", "type" => null]; - } else { - throw ValueException::invalid_kind($sql, "query"); - } - } else { - if (!is_string($sql)) $sql = strval($sql); - if (_query_create::isa($sql)) { - $meta = ["isa" => "create", "type" => "ddl"]; - } elseif (_query_select::isa($sql)) { - $meta = ["isa" => "select", "type" => "dql"]; - } elseif (_query_insert::isa($sql)) { - $meta = ["isa" => "insert", "type" => "dml"]; - } elseif (_query_update::isa($sql)) { - $meta = ["isa" => "update", "type" => "dml"]; - } elseif (_query_delete::isa($sql)) { - $meta = ["isa" => "delete", "type" => "dml"]; - } elseif (_query_generic::isa($sql)) { - $meta = ["isa" => "generic", "type" => null]; - } else { - $meta = ["isa" => "generic", "type" => null]; - } - } - } -} diff --git a/php/src/db/mysql/_query_create.php b/php/src/db/mysql/_query_create.php deleted file mode 100644 index 11f6602..0000000 --- a/php/src/db/mysql/_query_create.php +++ /dev/null @@ -1,10 +0,0 @@ -db(); - $query = new _query_base($query, $params); + $query = new query($query, $params); if ($query->useStmt($db, $stmt, $sql)) { if ($stmt->execute() === false) return false; if ($query->isInsert()) return $db->lastInsertId(); @@ -222,7 +222,7 @@ class Pdo implements IDatabase { function get($query, ?array $params=null, bool $entireRow=false) { $db = $this->db(); - $query = new _query_base($query, $params); + $query = new query($query, $params); $stmt = null; try { /** @var \PDOStatement $stmt */ @@ -251,7 +251,7 @@ class Pdo implements IDatabase { */ function all($query, ?array $params=null, $primaryKeys=null): Generator { $db = $this->db(); - $query = new _query_base($query, $params); + $query = new query($query, $params); $stmt = null; try { /** @var \PDOStatement $stmt */ diff --git a/php/src/db/pdo/_query_create.php b/php/src/db/pdo/_query_create.php deleted file mode 100644 index 997349a..0000000 --- a/php/src/db/pdo/_query_create.php +++ /dev/null @@ -1,10 +0,0 @@ - "create", "type" => "ddl"]; - } elseif (_query_select::isa($prefix)) { - $sql = _query_select::parse($sql, $bindinds); + } elseif (_select::isa($prefix)) { + $sql = _select::parse($sql, $bindings); $meta = ["isa" => "select", "type" => "dql"]; - } elseif (_query_insert::isa($prefix)) { - $sql = _query_insert::parse($sql, $bindinds); + } elseif (_insert::isa($prefix)) { + $sql = _insert::parse($sql, $bindings); $meta = ["isa" => "insert", "type" => "dml"]; - } elseif (_query_update::isa($prefix)) { - $sql = _query_update::parse($sql, $bindinds); + } elseif (_update::isa($prefix)) { + $sql = _update::parse($sql, $bindings); $meta = ["isa" => "update", "type" => "dml"]; - } elseif (_query_delete::isa($prefix)) { - $sql = _query_delete::parse($sql, $bindinds); + } elseif (_delete::isa($prefix)) { + $sql = _delete::parse($sql, $bindings); $meta = ["isa" => "delete", "type" => "dml"]; - } elseif (_query_generic::isa($prefix)) { - $sql = _query_generic::parse($sql, $bindinds); + } elseif (_generic::isa($prefix)) { + $sql = _generic::parse($sql, $bindings); $meta = ["isa" => "generic", "type" => null]; } else { throw ValueException::invalid_kind($sql, "query"); } } else { if (!is_string($sql)) $sql = strval($sql); - if (_query_create::isa($sql)) { + if (_create::isa($sql)) { $meta = ["isa" => "create", "type" => "ddl"]; - } elseif (_query_select::isa($sql)) { + } elseif (_select::isa($sql)) { $meta = ["isa" => "select", "type" => "dql"]; - } elseif (_query_insert::isa($sql)) { + } elseif (_insert::isa($sql)) { $meta = ["isa" => "insert", "type" => "dml"]; - } elseif (_query_update::isa($sql)) { + } elseif (_update::isa($sql)) { $meta = ["isa" => "update", "type" => "dml"]; - } elseif (_query_delete::isa($sql)) { + } elseif (_delete::isa($sql)) { $meta = ["isa" => "delete", "type" => "dml"]; - } elseif (_query_generic::isa($sql)) { + } elseif (_generic::isa($sql)) { $meta = ["isa" => "generic", "type" => null]; } else { $meta = ["isa" => "generic", "type" => null]; diff --git a/php/src/db/sqlite/Sqlite.php b/php/src/db/sqlite/Sqlite.php index a1ebf5c..cf14dcd 100644 --- a/php/src/db/sqlite/Sqlite.php +++ b/php/src/db/sqlite/Sqlite.php @@ -187,7 +187,7 @@ class Sqlite implements IDatabase { function exec($query, ?array $params=null) { $db = $this->db(); - $query = new _query_base($query, $params); + $query = new query($query, $params); if ($query->useStmt($db, $stmt, $sql)) { try { $result = $stmt->execute(); @@ -274,7 +274,7 @@ class Sqlite implements IDatabase { function get($query, ?array $params=null, bool $entireRow=false) { $db = $this->db(); - $query = new _query_base($query, $params); + $query = new query($query, $params); if ($query->useStmt($db, $stmt, $sql)) { try { $result = $this->checkResult($stmt->execute()); @@ -323,7 +323,7 @@ class Sqlite implements IDatabase { */ function all($query, ?array $params=null, $primaryKeys=null): iterable { $db = $this->db(); - $query = new _query_base($query, $params); + $query = new query($query, $params); if ($query->useStmt($db, $stmt, $sql)) { $result = $this->checkResult($stmt->execute()); return $this->_fetchResult($result, $stmt, $primaryKeys); diff --git a/php/src/db/sqlite/SqliteStorage.php b/php/src/db/sqlite/SqliteStorage.php index 287a9f7..b29639b 100644 --- a/php/src/db/sqlite/SqliteStorage.php +++ b/php/src/db/sqlite/SqliteStorage.php @@ -24,7 +24,7 @@ class SqliteStorage extends CapacitorStorage { ]; function _getCreateSql(CapacitorChannel $channel): string { - $query = new _query_base($this->_createSql($channel)); + $query = new query($this->_createSql($channel)); return self::format_sql($channel, $query->getSql()); } diff --git a/php/src/db/sqlite/_query_create.php b/php/src/db/sqlite/_query_create.php deleted file mode 100644 index 5aa7aa1..0000000 --- a/php/src/db/sqlite/_query_create.php +++ /dev/null @@ -1,10 +0,0 @@ - null], $sql, $params); + query::parse_conds(["col" => null], $sql, $params); self::assertSame(["col is null"], $sql); self::assertNull($params); $sql = $params = null; - _query_base::parse_conds(["col = 'value'"], $sql, $params); + query::parse_conds(["col = 'value'"], $sql, $params); self::assertSame(["col = 'value'"], $sql); self::assertNull($params); $sql = $params = null; - _query_base::parse_conds([["col = 'value'"]], $sql, $params); + query::parse_conds([["col = 'value'"]], $sql, $params); self::assertSame(["col = 'value'"], $sql); self::assertNull($params); $sql = $params = null; - _query_base::parse_conds(["int" => 42, "string" => "value"], $sql, $params); + query::parse_conds(["int" => 42, "string" => "value"], $sql, $params); self::assertSame(["(int = :int and string = :string)"], $sql); self::assertSame(["int" => 42, "string" => "value"], $params); $sql = $params = null; - _query_base::parse_conds(["or", "int" => 42, "string" => "value"], $sql, $params); + query::parse_conds(["or", "int" => 42, "string" => "value"], $sql, $params); self::assertSame(["(int = :int or string = :string)"], $sql); self::assertSame(["int" => 42, "string" => "value"], $params); $sql = $params = null; - _query_base::parse_conds([["int" => 42, "string" => "value"], ["int" => 24, "string" => "eulav"]], $sql, $params); + query::parse_conds([["int" => 42, "string" => "value"], ["int" => 24, "string" => "eulav"]], $sql, $params); self::assertSame(["((int = :int and string = :string) and (int = :int2 and string = :string2))"], $sql); self::assertSame(["int" => 42, "string" => "value", "int2" => 24, "string2" => "eulav"], $params); $sql = $params = null; - _query_base::parse_conds(["int" => ["is null"], "string" => ["<>", "value"]], $sql, $params); + query::parse_conds(["int" => ["is null"], "string" => ["<>", "value"]], $sql, $params); self::assertSame(["(int is null and string <> :string)"], $sql); self::assertSame(["string" => "value"], $params); $sql = $params = null; - _query_base::parse_conds(["col" => ["between", "lower", "upper"]], $sql, $params); + query::parse_conds(["col" => ["between", "lower", "upper"]], $sql, $params); self::assertSame(["col between :col and :col2"], $sql); self::assertSame(["col" => "lower", "col2" => "upper"], $params); $sql = $params = null; - _query_base::parse_conds(["col" => ["in", "one"]], $sql, $params); + query::parse_conds(["col" => ["in", "one"]], $sql, $params); self::assertSame(["col in (:col)"], $sql); self::assertSame(["col" => "one"], $params); $sql = $params = null; - _query_base::parse_conds(["col" => ["in", ["one", "two"]]], $sql, $params); + query::parse_conds(["col" => ["in", ["one", "two"]]], $sql, $params); self::assertSame(["col in (:col, :col2)"], $sql); self::assertSame(["col" => "one", "col2" => "two"], $params); $sql = $params = null; - _query_base::parse_conds(["col" => ["=", ["one", "two"]]], $sql, $params); + query::parse_conds(["col" => ["=", ["one", "two"]]], $sql, $params); self::assertSame(["col = :col and col = :col2"], $sql); self::assertSame(["col" => "one", "col2" => "two"], $params); $sql = $params = null; - _query_base::parse_conds(["or", "col" => ["=", ["one", "two"]]], $sql, $params); + query::parse_conds(["or", "col" => ["=", ["one", "two"]]], $sql, $params); self::assertSame(["col = :col or col = :col2"], $sql); self::assertSame(["col" => "one", "col2" => "two"], $params); $sql = $params = null; - _query_base::parse_conds(["col" => ["<>", ["one", "two"]]], $sql, $params); + query::parse_conds(["col" => ["<>", ["one", "two"]]], $sql, $params); self::assertSame(["col <> :col and col <> :col2"], $sql); self::assertSame(["col" => "one", "col2" => "two"], $params); $sql = $params = null; - _query_base::parse_conds(["or", "col" => ["<>", ["one", "two"]]], $sql, $params); + query::parse_conds(["or", "col" => ["<>", ["one", "two"]]], $sql, $params); self::assertSame(["col <> :col or col <> :col2"], $sql); self::assertSame(["col" => "one", "col2" => "two"], $params); } function testParseValues(): void { $sql = $params = null; - _query_base::parse_set_values(null, $sql, $params); + query::parse_set_values(null, $sql, $params); self::assertNull($sql); self::assertNull($params); $sql = $params = null; - _query_base::parse_set_values([], $sql, $params); + query::parse_set_values([], $sql, $params); self::assertNull($sql); self::assertNull($params); $sql = $params = null; - _query_base::parse_set_values(["col = 'value'"], $sql, $params); + query::parse_set_values(["col = 'value'"], $sql, $params); self::assertSame(["col = 'value'"], $sql); self::assertNull($params); $sql = $params = null; - _query_base::parse_set_values([["col = 'value'"]], $sql, $params); + query::parse_set_values([["col = 'value'"]], $sql, $params); self::assertSame(["col = 'value'"], $sql); self::assertNull($params); $sql = $params = null; - _query_base::parse_set_values(["int" => 42, "string" => "value"], $sql, $params); + query::parse_set_values(["int" => 42, "string" => "value"], $sql, $params); self::assertSame(["int = :int", "string = :string"], $sql); self::assertSame(["int" => 42, "string" => "value"], $params); $sql = $params = null; - _query_base::parse_set_values(["int" => 42, "string" => "value"], $sql, $params); + query::parse_set_values(["int" => 42, "string" => "value"], $sql, $params); self::assertSame(["int = :int", "string = :string"], $sql); self::assertSame(["int" => 42, "string" => "value"], $params); $sql = $params = null; - _query_base::parse_set_values([["int" => 42, "string" => "value"], ["int" => 24, "string" => "eulav"]], $sql, $params); + query::parse_set_values([["int" => 42, "string" => "value"], ["int" => 24, "string" => "eulav"]], $sql, $params); self::assertSame(["int = :int", "string = :string", "int = :int2", "string = :string2"], $sql); self::assertSame(["int" => 42, "string" => "value", "int2" => 24, "string2" => "eulav"], $params); }