diff --git a/src/db/filters.php b/src/db/filters.php deleted file mode 100644 index 68f9ce6..0000000 --- a/src/db/filters.php +++ /dev/null @@ -1,5 +0,0 @@ -get($query, $params, true); + } + protected function _fetchResult(SQLite3Result $result): Generator { try { while (($row = $result->fetchArray(SQLITE3_ASSOC)) !== false) { diff --git a/src/db/sqlite/_Query.php b/src/db/sqlite/_Query.php index 6e1d8db..28d0c0b 100644 --- a/src/db/sqlite/_Query.php +++ b/src/db/sqlite/_Query.php @@ -53,6 +53,120 @@ class _Query { 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_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_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", @@ -77,7 +191,7 @@ class _Query { "having" => "?array", ]; static function is_select(string $sql): bool { - return false; + return preg_match("/^select\b/i", $sql); } /** @@ -87,14 +201,14 @@ class _Query { 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 + ### vérifier la présence des parties nécessaires $sql = []; if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix; - # select + ## select self::consume('select\s*', $tmpsql); $sql[] = "select"; - # cols + ## cols $usercols = []; - if (self::consume('(.*)\s*(?<=$|\bfrom\b)', $tmpsql, $ms)) { + if (self::consume('(.*?)\s*(?=$|\bfrom\b)', $tmpsql, $ms)) { if ($ms[1]) $usercols[] = $ms[1]; } $tmpcols = cl::withn($query["cols"] ?? null); @@ -119,24 +233,31 @@ class _Query { $usercols = array_merge($usercols, $cols); } } + if (!$usercols && !$cols) $usercols = ["*"]; $sql[] = implode(" ", $usercols); - # from + ## from $from = $query["from"] ?? null; - if (self::consume('from\s+([a-z_][a-z0-9_]*)\s*(?<=;?\s*$|\bwhere\b)', $tmpsql, $ms)) { + 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 + ## where $userwhere = []; - if (self::consume('where\s*(.*)(?<=;?\s*$|\border\s+by\b)', $tmpsql, $ms)) { + if (self::consume('where\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 # group by # having diff --git a/tests/db/sqlite/SqliteTest.php b/tests/db/sqlite/SqliteTest.php index 637758e..d3f0f11 100644 --- a/tests/db/sqlite/SqliteTest.php +++ b/tests/db/sqlite/SqliteTest.php @@ -3,7 +3,6 @@ namespace nur\sery\db\sqlite; use Exception; use nulib\tests\TestCase; -use nur\sery\ValueException; class SqliteTest extends TestCase { const CREATE_PERSON = "create table person(nom varchar, prenom varchar, age integer)"; @@ -70,4 +69,52 @@ class SqliteTest extends TestCase { ["i" => 10, "s" => null/*"dix"*/], ], iterator_to_array($sqlite->all("select * from mapping"))); } + + function testSelect() { + $sqlite = new Sqlite(":memory:", [ + "migrate" => "create table user (name varchar, amount integer)", + ]); + $sqlite->exec(["insert into user", "values" => ["name" => "jclain1", "amount" => 1]]); + $sqlite->exec(["insert into user", "values" => ["name" => "jclain2", "amount" => 2]]); + $sqlite->exec(["insert into user", "values" => ["name" => "jclain5", "amount" => 5]]); + $sqlite->exec(["insert into user", "values" => ["name" => "fclain7", "amount" => 7]]); + $sqlite->exec(["insert into user", "values" => ["name" => "fclain9", "amount" => 9]]); + $sqlite->exec(["insert into user", "values" => ["name" => "fclain10", "amount" => 10]]); + self::assertSame([ + "name" => "jclain1", + "amount" => 1, + ], $sqlite->one("select * from user where name = 'jclain1'")); + self::assertSame([ + "name" => "jclain1", + "amount" => 1, + ], $sqlite->one(["select * from user where name = 'jclain1'"])); + self::assertSame([ + "name" => "jclain1", + "amount" => 1, + ], $sqlite->one(["select from user where name = 'jclain1'"])); + self::assertSame([ + "name" => "jclain1", + "amount" => 1, + ], $sqlite->one(["select from user", "where" => ["name = 'jclain1'"]])); + self::assertSame([ + "name" => "jclain1", + "amount" => 1, + ], $sqlite->one(["select", "from" => "user", "where" => ["name = 'jclain1'"]])); + self::assertSame([ + "name" => "jclain1", + "amount" => 1, + ], $sqlite->one(["select", "from" => "user", "where" => ["name" => "jclain1"]])); + self::assertSame([ + "name" => "jclain1", + ], $sqlite->one(["select name", "from" => "user", "where" => ["name" => "jclain1"]])); + self::assertSame([ + "name" => "jclain1", + ], $sqlite->one(["select", "cols" => "name", "from" => "user", "where" => ["name" => "jclain1"]])); + self::assertSame([ + "name" => "jclain1", + ], $sqlite->one(["select", "cols" => ["name"], "from" => "user", "where" => ["name" => "jclain1"]])); + self::assertSame([ + "plouf" => "jclain1", + ], $sqlite->one(["select", "cols" => ["plouf" => "name"], "from" => "user", "where" => ["name" => "jclain1"]])); + } } diff --git a/tests/db/sqlite/_QueryTest.php b/tests/db/sqlite/_QueryTest.php new file mode 100644 index 0000000..d3bb82c --- /dev/null +++ b/tests/db/sqlite/_QueryTest.php @@ -0,0 +1,86 @@ + 42, "string" => "value"], $sql, $params); + self::assertSame(["(int = :int and string = :string)"], $sql); + self::assertSame(["int" => 42, "string" => "value"], $params); + + $sql = $params = null; + _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::parse_conds([["int" => 42, "string" => "value"], ["int" => 24, "string" => "eulav"]], $sql, $params); + self::assertSame(["((int = :int and string = :string) and (int = :int1 and string = :string1))"], $sql); + self::assertSame(["int" => 42, "string" => "value", "int1" => 24, "string1" => "eulav"], $params); + + $sql = $params = null; + _Query::parse_conds(["int" => ["is null"], "string" => ["<>", "value"]], $sql, $params); + self::assertSame(["(int is null and string <> :string)"], $sql); + self::assertSame(["string" => "value"], $params); + } + + function testParseValues(): void { + $sql = $params = null; + _Query::parse_values(null, $sql, $params); + self::assertNull($sql); + self::assertNull($params); + + $sql = $params = null; + _Query::parse_values([], $sql, $params); + self::assertNull($sql); + self::assertNull($params); + + $sql = $params = null; + _Query::parse_values(["col = 'value'"], $sql, $params); + self::assertSame(["col = 'value'"], $sql); + self::assertNull($params); + + $sql = $params = null; + _Query::parse_values([["col = 'value'"]], $sql, $params); + self::assertSame(["col = 'value'"], $sql); + self::assertNull($params); + + $sql = $params = null; + _Query::parse_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::parse_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::parse_values([["int" => 42, "string" => "value"], ["int" => 24, "string" => "eulav"]], $sql, $params); + self::assertSame(["int = :int", "string = :string", "int = :int1", "string = :string1"], $sql); + self::assertSame(["int" => 42, "string" => "value", "int1" => 24, "string1" => "eulav"], $params); + } + +}