From 628067d1e1adafa7b78e5a15e92051fb6723c59c Mon Sep 17 00:00:00 2001 From: gtbu Date: Sun, 6 Apr 2025 18:01:44 +0200 Subject: [PATCH] update Medoo 2.2 Lightweight PHP Database Framework 2.2 --- include/thirdparty/db/LICENSE.md | 2 +- include/thirdparty/db/Medoo.php | 205 +- include/thirdparty/db/Medoo2.1.6.php | 2233 ----------------- include/thirdparty/db/tests/AggregateTest.php | 117 + include/thirdparty/db/tests/CreateTest.php | 142 ++ include/thirdparty/db/tests/DeleteTest.php | 59 + include/thirdparty/db/tests/DropTest.php | 51 + include/thirdparty/db/tests/GetTest.php | 163 ++ include/thirdparty/db/tests/HasTest.php | 32 + include/thirdparty/db/tests/InsertTest.php | 242 ++ include/thirdparty/db/tests/MedooTestCase.php | 82 + include/thirdparty/db/tests/QueryTest.php | 152 ++ include/thirdparty/db/tests/QuoteTest.php | 123 + include/thirdparty/db/tests/RandTest.php | 153 ++ include/thirdparty/db/tests/RawTest.php | 57 + include/thirdparty/db/tests/ReplaceTest.php | 55 + include/thirdparty/db/tests/SelectTest.php | 715 ++++++ include/thirdparty/db/tests/UpdateTest.php | 91 + include/thirdparty/db/tests/WhereTest.php | 1177 +++++++++ 19 files changed, 3545 insertions(+), 2306 deletions(-) delete mode 100644 include/thirdparty/db/Medoo2.1.6.php create mode 100644 include/thirdparty/db/tests/AggregateTest.php create mode 100644 include/thirdparty/db/tests/CreateTest.php create mode 100644 include/thirdparty/db/tests/DeleteTest.php create mode 100644 include/thirdparty/db/tests/DropTest.php create mode 100644 include/thirdparty/db/tests/GetTest.php create mode 100644 include/thirdparty/db/tests/HasTest.php create mode 100644 include/thirdparty/db/tests/InsertTest.php create mode 100644 include/thirdparty/db/tests/MedooTestCase.php create mode 100644 include/thirdparty/db/tests/QueryTest.php create mode 100644 include/thirdparty/db/tests/QuoteTest.php create mode 100644 include/thirdparty/db/tests/RandTest.php create mode 100644 include/thirdparty/db/tests/RawTest.php create mode 100644 include/thirdparty/db/tests/ReplaceTest.php create mode 100644 include/thirdparty/db/tests/SelectTest.php create mode 100644 include/thirdparty/db/tests/UpdateTest.php create mode 100644 include/thirdparty/db/tests/WhereTest.php diff --git a/include/thirdparty/db/LICENSE.md b/include/thirdparty/db/LICENSE.md index b2eb2fb..b71081a 100644 --- a/include/thirdparty/db/LICENSE.md +++ b/include/thirdparty/db/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 Angel Lai +Copyright (c) 2025 Angel Lai Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/include/thirdparty/db/Medoo.php b/include/thirdparty/db/Medoo.php index c466bb1..8aa434a 100644 --- a/include/thirdparty/db/Medoo.php +++ b/include/thirdparty/db/Medoo.php @@ -6,10 +6,10 @@ declare(strict_types=1); * * The Lightweight PHP Database Framework to Accelerate Development. * - * @version 2.1.12 - * @author Angel Lai + * @version 2.2.0 * @package Medoo - * @copyright Copyright 2024 Medoo Project, Angel Lai. + * @author Angel Lai + * @copyright Angel Lai * @license https://opensource.org/licenses/MIT * @link https://medoo.in */ @@ -43,10 +43,18 @@ class Raw } /** + * @method array select(string $table, array $columns) + * @method mixed select(string $table, string $column) * @method array select(string $table, array $columns, array $where) + * @method mixed select(string $table, string $column, array $where) + * @method array select(string $table, array $join, array $columns) + * @method mixed select(string $table, array $join, string $column) * @method null select(string $table, array $columns, callable $callback) + * @method null select(string $table, string $column, callable $callback) * @method null select(string $table, array $columns, array $where, callable $callback) + * @method null select(string $table, string $column, array $where, callable $callback) * @method null select(string $table, array $join, array $columns, array $where, callable $callback) + * @method null select(string $table, array $join, string $column, array $where, callable $callback) * @method mixed get(string $table, array|string $columns, array $where) * @method bool has(string $table, array $where) * @method mixed rand(string $table, array|string $column, array $where) @@ -63,28 +71,28 @@ class Raw class Medoo { /** - * The PDO object. + * The PDO database connection instance. * * @var \PDO */ public $pdo; /** - * The type of database. + * The database type. * * @var string */ public $type; /** - * Table prefix. + * The table prefix. * * @var string */ protected $prefix; /** - * The PDO statement object. + * Current PDO statement instance. * * @var \PDOStatement */ @@ -98,88 +106,125 @@ class Medoo protected $dsn; /** - * The array of logs. + * Logged queries. * * @var array */ protected $logs = []; /** - * Determine should log the query or not. + * Whether query logging is enabled. * * @var bool */ protected $logging = false; /** - * Determine is in test mode. + * Whether the database is in test mode. * * @var bool */ protected $testMode = false; /** - * The last query string was generated in test mode. + * The last generated query string in test mode. * * @var string */ public $queryString; /** - * Determine is in debug mode. + * Whether debug mode is enabled. * * @var bool */ protected $debugMode = false; /** - * Determine should save debug logging. + * Whether debug logging is enabled. * * @var bool */ protected $debugLogging = false; /** - * The array of logs for debugging. + * Logged debug queries. * * @var array */ protected $debugLogs = []; /** - * The unique global id. + * The unique global identifier. * - * @var integer + * @var int */ protected $guid = 0; /** - * The returned id for the insert. + * The last inserted record ID. * * @var string */ public $returnId = ''; /** - * Error Message. + * The last error message. * * @var string|null */ public $error = null; /** - * The array of error information. + * The last error details. * * @var array|null */ public $errorInfo = null; /** - * Connect the database. + * The connector used for table aliases. * + * @var string + */ + protected $tableAliasConnector = ' AS '; + + /** + * The pattern used for quoting identifiers. + * + * @var string + */ + protected $quotePattern = '"$1"'; + + /** + * Regular expression pattern for valid table names. + * + * @var string + */ + protected const TABLE_PATTERN = "[\p{L}_][\p{L}\p{N}@$#\-_]*"; + + /** + * Regular expression pattern for valid column names. + * + * @var string + */ + protected const COLUMN_PATTERN = "[\p{L}_][\p{L}\p{N}@$#\-_\.]*"; + + /** + * Regular expression pattern for valid alias names. + * + * @var string + */ + protected const ALIAS_PATTERN = "[\p{L}_][\p{L}\p{N}@$#\-_]*"; + + /** + * Establish a database connection. + * + * Example usage: + * * ``` * $database = new Medoo([ - * // required + * // Required * 'type' => 'mysql', * 'database' => 'name', * 'host' => 'localhost', @@ -195,7 +240,7 @@ class Medoo * * @param array $options Connection options * @return Medoo - * @throws PDOException + * @throws PDOException If the connection fails * @link https://medoo.in/api/new * @codeCoverageIgnore */ @@ -211,7 +256,11 @@ class Medoo return; } - $options['type'] = $options['type'] ?? $options['database_type']; + $options['type'] = $options['type'] ?? $options['database_type'] ?? null; + + if (!$options['type']) { + throw new InvalidArgumentException('Database type is required.'); + } if (!isset($options['pdo'])) { $options['database'] = $options['database'] ?? $options['database_name']; @@ -221,19 +270,12 @@ class Medoo } } - if (isset($options['type'])) { - $this->type = strtolower($options['type']); - - if ($this->type === 'mariadb') { - $this->type = 'mysql'; - } - } + $this->setupType($options['type']); if (isset($options['logging']) && is_bool($options['logging'])) { $this->logging = $options['logging']; } - $option = $options['option'] ?? []; $commands = []; switch ($this->type) { @@ -275,10 +317,7 @@ class Medoo throw new InvalidArgumentException('Invalid DSN option supplied.'); } } else { - if ( - isset($options['port']) && - is_int($options['port'] * 1) - ) { + if (isset($options['port']) && is_numeric($options['port'])) { $port = $options['port']; } @@ -451,7 +490,7 @@ class Medoo $dsn, $options['username'] ?? null, $options['password'] ?? null, - $option + $options['option'] ?? [] ); if (isset($options['error'])) { @@ -479,6 +518,31 @@ class Medoo } } + /** + * Setup the database type. + * + * @param string The database type string. + * @return void + */ + public function setupType(string $type) + { + $databaseType = strtolower($type); + + if ($databaseType === 'mariadb') { + $databaseType = 'mysql'; + } + + if ($databaseType === 'oracle') { + $this->tableAliasConnector = ' '; + } elseif ($databaseType === 'mysql') { + $this->quotePattern = '`$1`'; + } elseif ($databaseType === 'mssql') { + $this->quotePattern = '[$1]'; + } + + $this->type = $databaseType; + } + /** * Generate a new map key for the placeholder. * @@ -512,7 +576,7 @@ class Medoo * @codeCoverageIgnore * @return \PDOStatement|null */ - public function exec(string $statement, array $map = [], callable $callback = null): ?PDOStatement + public function exec(string $statement, array $map = [], ?callable $callback = null): ?PDOStatement { $this->statement = null; $this->errorInfo = null; @@ -591,14 +655,9 @@ class Medoo */ protected function generate(string $statement, array $map): string { - $identifier = [ - 'mysql' => '`$1`', - 'mssql' => '[$1]' - ]; - $statement = preg_replace( - '/(?!\'[^\s]+\s?)"([\p{L}_][\p{L}\p{N}@$#\-_]*)"(?!\s?[^\s]+\')/u', - $identifier[$this->type] ?? '"$1"', + '/(?!\'[^\s]+\s?)"(' . $this::COLUMN_PATTERN . ')"(?!\s?[^\s]+\')/u', + $this->quotePattern, $statement ); @@ -661,7 +720,7 @@ class Medoo } $query = preg_replace_callback( - '/(([`\'])[\<]*?)?((FROM|TABLE|INTO|UPDATE|JOIN|TABLE IF EXISTS)\s*)?\<(([\p{L}_][\p{L}\p{N}@$#\-_]*)(\.[\p{L}_][\p{L}\p{N}@$#\-_]*)?)\>([^,]*?\2)?/', + '/(([`\'])[\<]*?)?((FROM|TABLE|TABLES LIKE|INTO|UPDATE|JOIN|TABLE IF EXISTS)\s*)?\<((' . $this::TABLE_PATTERN . ')(\.' . $this::COLUMN_PATTERN . ')?)\>([^,]*?\2)?/', function ($matches) { if (!empty($matches[2]) && isset($matches[8])) { return $matches[0]; @@ -688,9 +747,9 @@ class Medoo } /** - * Quote a string for use in a query. + * Escape and quote a string for use in an SQL query. * - * @param string $string + * @param string $string The string to be quoted. * @return string */ public function quote(string $string): string @@ -703,14 +762,15 @@ class Medoo } /** - * Quote table name for use in a query. + * Quote a table name for use in an SQL query. * - * @param string $table + * @param string $table The table name to be quoted. * @return string + * @throws InvalidArgumentException If the table name is invalid. */ public function tableQuote(string $table): string { - if (preg_match('/^[\p{L}_][\p{L}\p{N}@$#\-_]*$/u', $table)) { + if (preg_match("/^" . $this::TABLE_PATTERN . "$/u", $table)) { return '"' . $this->prefix . $table . '"'; } @@ -718,14 +778,15 @@ class Medoo } /** - * Quote column name for use in a query. + * Quote a column name for use in an SQL query. * - * @param string $column + * @param string $column The column name to be quoted. * @return string + * @throws InvalidArgumentException If the column name is invalid. */ public function columnQuote(string $column): string { - if (preg_match('/^[\p{L}_][\p{L}\p{N}@$#\-_]*(\.?[\p{L}_][\p{L}\p{N}@$#\-_]*)?$/u', $column)) { + if (preg_match("/^" . $this::TABLE_PATTERN . "(\.?" . $this::ALIAS_PATTERN . ")?$/u", $column)) { return strpos($column, '.') !== false ? '"' . $this->prefix . str_replace('.', '"."', $column) . '"' : '"' . $column . '"'; @@ -794,14 +855,14 @@ class Medoo } elseif ($isArrayValue) { $stack[] = $this->columnPush($value, $map, false, $isJoin); } elseif (!$isIntKey && $raw = $this->buildRaw($value, $map)) { - preg_match('/(?[\p{L}_][\p{L}\p{N}@$#\-_\.]*)(\s*\[(?(String|Bool|Int|Number))\])?/u', $key, $match); + preg_match("/(?" . $this::COLUMN_PATTERN . ")(\s*\[(?(String|Bool|Int|Number))\])?/u", $key, $match); $stack[] = "{$raw} AS {$this->columnQuote($match['column'])}"; } elseif ($isIntKey && is_string($value)) { if ($isJoin && strpos($value, '*') !== false) { throw new InvalidArgumentException('Cannot use table.* to select all columns while joining table.'); } - preg_match('/(?[\p{L}_][\p{L}\p{N}@$#\-_\.]*)(?:\s*\((?[\p{L}_][\p{L}\p{N}@$#\-_]*)\))?(?:\s*\[(?(?:String|Bool|Int|Number|Object|JSON))\])?/u', $value, $match); + preg_match("/(?" . $this::COLUMN_PATTERN . ")(?:\s*\((?" . $this::ALIAS_PATTERN . ")\))?(?:\s*\[(?(?:String|Bool|Int|Number|Object|JSON))\])?/u", $value, $match); $columnString = ''; @@ -858,20 +919,20 @@ class Medoo $isIndex = is_int($key); preg_match( - '/([\p{L}_][\p{L}\p{N}@$#\-_\.]*)(\[(?.*)\])?([\p{L}_][\p{L}\p{N}@$#\-_\.]*)?/u', + "/(?" . $this::COLUMN_PATTERN . ")(\[(?.*)\])?(?" . $this::COLUMN_PATTERN . ")?/u", $isIndex ? $value : $key, $match ); - $column = $this->columnQuote($match[1]); + $column = $this->columnQuote($match['column']); $operator = $match['operator'] ?? null; - if ($isIndex && isset($match[4]) && in_array($operator, ['>', '>=', '<', '<=', '=', '!='])) { - $stack[] = "{$column} {$operator} " . $this->columnQuote($match[4]); + if ($isIndex && isset($match['comparison']) && in_array($operator, ['>', '>=', '<', '<=', '=', '!='])) { + $stack[] = "{$column} {$operator} " . $this->columnQuote($match['comparison']); continue; } - if ($operator && $operator != '=') { + if ($operator && $operator !== '=') { if (in_array($operator, ['>', '>=', '<', '<='])) { $condition = "{$column} {$operator} "; @@ -1182,12 +1243,12 @@ class Medoo $where = null, $columnFn = null ): string { - preg_match('/(?[\p{L}_][\p{L}\p{N}@$#\-_]*)\s*\((?[\p{L}_][\p{L}\p{N}@$#\-_]*)\)/u', $table, $tableMatch); + preg_match("/(?
" . $this::TABLE_PATTERN . ")\s*\((?" . $this::ALIAS_PATTERN . ")\)/u", $table, $tableMatch); if (isset($tableMatch['table'], $tableMatch['alias'])) { $table = $this->tableQuote($tableMatch['table']); $tableAlias = $this->tableQuote($tableMatch['alias']); - $tableQuery = "{$table} AS {$tableAlias}"; + $tableQuery = "{$table}{$this->tableAliasConnector}{$tableAlias}"; } else { $table = $this->tableQuote($table); $tableQuery = $table; @@ -1283,7 +1344,7 @@ class Medoo ]; foreach ($join as $subtable => $relation) { - preg_match('/(\[(?\<\>?|\>\[\p{L}_][\p{L}\p{N}@$#\-_]*)\s?(\((?[\p{L}_][\p{L}\p{N}@$#\-_]*)\))?/u', $subtable, $match); + preg_match("/(\[(?\<\>?|\>\" . $this::TABLE_PATTERN . ")\s?(\((?" . $this::ALIAS_PATTERN . ")\))?/u", $subtable, $match); if ($match['join'] === '' || $match['table'] === '') { continue; @@ -1325,7 +1386,7 @@ class Medoo $tableName = $this->tableQuote($match['table']); if (isset($match['alias'])) { - $tableName .= ' AS ' . $this->tableQuote($match['alias']); + $tableName .= $this->tableAliasConnector . $this->tableQuote($match['alias']); } $tableJoin[] = $type[$match['join']] . " JOIN {$tableName} {$relation}"; @@ -1350,7 +1411,7 @@ class Medoo foreach ($columns as $key => $value) { if (is_int($key)) { - preg_match('/([\p{L}_][\p{L}\p{N}@$#\-_]*\.)?(?[\p{L}_][\p{L}\p{N}@$#\-_]*)(?:\s*\((?[\p{L}_][\p{L}\p{N}@$#\-_]*)\))?(?:\s*\[(?(?:String|Bool|Int|Number|Object|JSON))\])?/u', $value, $keyMatch); + preg_match("/(" . $this::TABLE_PATTERN . "\.)?(?" . $this::COLUMN_PATTERN . ")(?:\s*\((?" . $this::ALIAS_PATTERN . ")\))?(?:\s*\[(?(?:String|Bool|Int|Number|Object|JSON))\])?/u", $value, $keyMatch); $columnKey = !empty($keyMatch['alias']) ? $keyMatch['alias'] : @@ -1360,7 +1421,7 @@ class Medoo [$columnKey, $keyMatch['type']] : [$columnKey]; } elseif ($this->isRaw($value)) { - preg_match('/([\p{L}_][\p{L}\p{N}@$#\-_]*\.)?(?[\p{L}_][\p{L}\p{N}@$#\-_]*)(\s*\[(?(String|Bool|Int|Number))\])?/u', $key, $keyMatch); + preg_match("/(" . $this::TABLE_PATTERN . "\.)?(?" . $this::COLUMN_PATTERN . ")(\s*\[(?(String|Bool|Int|Number))\])?/u", $key, $keyMatch); $columnKey = $keyMatch['column']; $stack[$key] = isset($keyMatch['type']) ? @@ -1396,14 +1457,14 @@ class Medoo array $columnMap, array &$stack, bool $root, - array &$result = null + ?array &$result = null ): void { if ($root) { $columnsKey = array_keys($columns); if (count($columnsKey) === 1 && is_array($columns[$columnsKey[0]])) { $indexKey = array_keys($columns)[0]; - $dataKey = preg_replace("/^[\p{L}_][\p{L}\p{N}@$#\-_]*\./u", '', $indexKey); + $dataKey = preg_replace("/^" . $this::COLUMN_PATTERN . "\./u", '', $indexKey); $currentStack = []; foreach ($data as $item) { @@ -1537,7 +1598,7 @@ class Medoo foreach ($columns as $name => $definition) { if (is_int($name)) { - $stack[] = preg_replace('/\<([\p{L}_][\p{L}\p{N}@$#\-_]*)\>/u', '"$1"', $definition); + $stack[] = preg_replace("/\<(" . $this::COLUMN_PATTERN . ")\>/u", '"$1"', $definition); } elseif (is_array($definition)) { $stack[] = $this->columnQuote($name) . ' ' . implode(' ', $definition); } elseif (is_string($definition)) { @@ -1668,7 +1729,7 @@ class Medoo * @param string $primaryKey * @return \PDOStatement|null */ - public function insert(string $table, array $values, string $primaryKey = null): ?PDOStatement + public function insert(string $table, array $values, ?string $primaryKey = null): ?PDOStatement { $stack = []; $columns = []; @@ -1792,7 +1853,7 @@ class Medoo continue; } - preg_match('/(?[\p{L}_][\p{L}\p{N}@$#\-_]*)(\[(?\+|\-|\*|\/)\])?/u', $key, $match); + preg_match("/" . $this::COLUMN_PATTERN . "(\[(?\+|\-|\*|\/)\])?/u", $key, $match); if (isset($match['operator'])) { if (is_numeric($value)) { @@ -2131,7 +2192,7 @@ class Medoo * @codeCoverageIgnore * @return string|null */ - public function id(string $name = null): ?string + public function id(?string $name = null): ?string { $type = $this->type; diff --git a/include/thirdparty/db/Medoo2.1.6.php b/include/thirdparty/db/Medoo2.1.6.php deleted file mode 100644 index 00cdb44..0000000 --- a/include/thirdparty/db/Medoo2.1.6.php +++ /dev/null @@ -1,2233 +0,0 @@ - 'mysql', - * 'database' => 'name', - * 'host' => 'localhost', - * 'username' => 'your_username', - * 'password' => 'your_password', - * - * // [optional] - * 'charset' => 'utf8mb4', - * 'port' => 3306, - * 'prefix' => 'PREFIX_' - * ]); - * ``` - * - * @param array $options Connection options - * @return Medoo - * @throws PDOException - * @link https://medoo.in/api/new - * @codeCoverageIgnore - */ - - public function __construct(array $options) - { - if (isset($options['prefix'])) { - $this->prefix = $options['prefix']; - } - - if (isset($options['testMode']) && $options['testMode'] == true) { - $this->testMode = true; - return; - } - - $options['type'] = $options['type'] ?? $options['database_type']; - - if (!isset($options['pdo'])) { - $options['database'] = $options['database'] ?? $options['database_name']; - - if (!isset($options['socket'])) { - $options['host'] = $options['host'] ?? $options['server'] ?? false; - } - } - - if (isset($options['type'])) { - $this->type = strtolower($options['type']); - - if ($this->type === 'mariadb') { - $this->type = 'mysql'; - } - } - - if (isset($options['logging']) && is_bool($options['logging'])) { - $this->logging = $options['logging']; - } - - $option = $options['option'] ?? []; - $commands = (isset($options['command']) && is_array($options['command'])) ? - $options['command'] : - []; - - switch ($this->type) { - - case 'mysql': - // Make MySQL using standard quoted identifier. - $commands[] = 'SET SQL_MODE=ANSI_QUOTES'; - - break; - - case 'mssql': - // Keep MSSQL QUOTED_IDENTIFIER is ON for standard quoting. - $commands[] = 'SET QUOTED_IDENTIFIER ON'; - - // Make ANSI_NULLS is ON for NULL value. - $commands[] = 'SET ANSI_NULLS ON'; - - break; - } - - if (isset($options['pdo'])) { - if (!$options['pdo'] instanceof PDO) { - throw new InvalidArgumentException('Invalid PDO object supplied.'); - } - - $this->pdo = $options['pdo']; - - foreach ($commands as $value) { - $this->pdo->exec($value); - } - - return; - } - - if (isset($options['dsn'])) { - if (is_array($options['dsn']) && isset($options['dsn']['driver'])) { - $attr = $options['dsn']; - } else { - throw new InvalidArgumentException('Invalid DSN option supplied.'); - } - } else { - if ( - isset($options['port']) && - is_int($options['port'] * 1) - ) { - $port = $options['port']; - } - - $isPort = isset($port); - - switch ($this->type) { - - case 'mysql': - $attr = [ - 'driver' => 'mysql', - 'dbname' => $options['database'] - ]; - - if (isset($options['socket'])) { - $attr['unix_socket'] = $options['socket']; - } else { - $attr['host'] = $options['host']; - - if ($isPort) { - $attr['port'] = $port; - } - } - - break; - - case 'pgsql': - $attr = [ - 'driver' => 'pgsql', - 'host' => $options['host'], - 'dbname' => $options['database'] - ]; - - if ($isPort) { - $attr['port'] = $port; - } - - break; - - case 'sybase': - $attr = [ - 'driver' => 'dblib', - 'host' => $options['host'], - 'dbname' => $options['database'] - ]; - - if ($isPort) { - $attr['port'] = $port; - } - - break; - - case 'oracle': - $attr = [ - 'driver' => 'oci', - 'dbname' => $options['host'] ? - '//' . $options['host'] . ($isPort ? ':' . $port : ':1521') . '/' . $options['database'] : - $options['database'] - ]; - - if (isset($options['charset'])) { - $attr['charset'] = $options['charset']; - } - - break; - - case 'mssql': - if (isset($options['driver']) && $options['driver'] === 'dblib') { - $attr = [ - 'driver' => 'dblib', - 'host' => $options['host'] . ($isPort ? ':' . $port : ''), - 'dbname' => $options['database'] - ]; - - if (isset($options['appname'])) { - $attr['appname'] = $options['appname']; - } - - if (isset($options['charset'])) { - $attr['charset'] = $options['charset']; - } - } else { - $attr = [ - 'driver' => 'sqlsrv', - 'Server' => $options['host'] . ($isPort ? ',' . $port : ''), - 'Database' => $options['database'] - ]; - - if (isset($options['appname'])) { - $attr['APP'] = $options['appname']; - } - - $config = [ - 'ApplicationIntent', - 'AttachDBFileName', - 'Authentication', - 'ColumnEncryption', - 'ConnectionPooling', - 'Encrypt', - 'Failover_Partner', - 'KeyStoreAuthentication', - 'KeyStorePrincipalId', - 'KeyStoreSecret', - 'LoginTimeout', - 'MultipleActiveResultSets', - 'MultiSubnetFailover', - 'Scrollable', - 'TraceFile', - 'TraceOn', - 'TransactionIsolation', - 'TransparentNetworkIPResolution', - 'TrustServerCertificate', - 'WSID', - ]; - - foreach ($config as $value) { - $keyname = strtolower(preg_replace(['/([a-z\d])([A-Z])/', '/([^_])([A-Z][a-z])/'], '$1_$2', $value)); - - if (isset($options[$keyname])) { - $attr[$value] = $options[$keyname]; - } - } - } - - break; - - case 'sqlite': - $attr = [ - 'driver' => 'sqlite', - $options['database'] - ]; - - break; - } - } - - if (!isset($attr)) { - throw new InvalidArgumentException('Incorrect connection options.'); - } - - $driver = $attr['driver']; - - if (!in_array($driver, PDO::getAvailableDrivers())) { - throw new InvalidArgumentException("Unsupported PDO driver: {$driver}."); - } - - unset($attr['driver']); - - $stack = []; - - foreach ($attr as $key => $value) { - $stack[] = is_int($key) ? $value : $key . '=' . $value; - } - - $dsn = $driver . ':' . implode(';', $stack); - - if ( - in_array($this->type, ['mysql', 'pgsql', 'sybase', 'mssql']) && - isset($options['charset']) - ) { - $commands[] = "SET NAMES '{$options['charset']}'" . ( - $this->type === 'mysql' && isset($options['collation']) ? - " COLLATE '{$options['collation']}'" : '' - ); - } - - $this->dsn = $dsn; - - try { - $this->pdo = new PDO( - $dsn, - $options['username'] ?? null, - $options['password'] ?? null, - $option - ); - - if (isset($options['error'])) { - $this->pdo->setAttribute( - PDO::ATTR_ERRMODE, - in_array($options['error'], [ - PDO::ERRMODE_SILENT, - PDO::ERRMODE_WARNING, - PDO::ERRMODE_EXCEPTION - ]) ? - $options['error'] : - PDO::ERRMODE_SILENT - ); - } - - foreach ($commands as $value) { - $this->pdo->exec($value); - } - } catch (PDOException $e) { - throw new PDOException($e->getMessage()); - } - } - - /** - * Generate a new map key for the placeholder. - * - * @return string - */ - protected function mapKey(): string - { - return ':MeD' . $this->guid++ . '_mK'; - } - - /** - * Execute customized raw statement. - * - * @param string $statement The raw SQL statement. - * @param array $map The array of input parameters value for prepared statement. - * @return \PDOStatement|null - */ - public function query(string $statement, array $map = []): ?PDOStatement - { - $raw = $this->raw($statement, $map); - $statement = $this->buildRaw($raw, $map); - - return $this->exec($statement, $map); - } - - /** - * Execute the raw statement. - * - * @param string $statement The SQL statement. - * @param array $map The array of input parameters value for prepared statement. - * @codeCoverageIgnore - * @return \PDOStatement|null - */ - public function exec(string $statement, array $map = [], callable $callback = null): ?PDOStatement - { - $this->statement = null; - $this->errorInfo = null; - $this->error = null; - - if ($this->testMode) { - $this->queryString = $this->generate($statement, $map); - return null; - } - - if ($this->debugMode) { - if ($this->debugLogging) { - $this->debugLogs[] = $this->generate($statement, $map); - return null; - } - - echo $this->generate($statement, $map); - - $this->debugMode = false; - - return null; - } - - if ($this->logging) { - $this->logs[] = [$statement, $map]; - } else { - $this->logs = [[$statement, $map]]; - } - - $statement = $this->pdo->prepare($statement); - $errorInfo = $this->pdo->errorInfo(); - - if ($errorInfo[0] !== '00000') { - $this->errorInfo = $errorInfo; - $this->error = $errorInfo[2]; - - return null; - } - - foreach ($map as $key => $value) { - $statement->bindValue($key, $value[0], $value[1]); - } - - if (is_callable($callback)) { - $this->pdo->beginTransaction(); - $callback($statement); - $execute = $statement->execute(); - $this->pdo->commit(); - } else { - $execute = $statement->execute(); - } - - $errorInfo = $statement->errorInfo(); - - if ($errorInfo[0] !== '00000') { - $this->errorInfo = $errorInfo; - $this->error = $errorInfo[2]; - - return null; - } - - if ($execute) { - $this->statement = $statement; - } - - return $statement; - } - - /** - * Generate readable statement. - * - * @param string $statement - * @param array $map - * @codeCoverageIgnore - * @return string - */ - protected function generate(string $statement, array $map): string - { - $identifier = [ - 'mysql' => '`$1`', - 'mssql' => '[$1]' - ]; - - $statement = preg_replace( - '/(?!\'[^\s]+\s?)"([\p{L}_][\p{L}\p{N}@$#\-_]*)"(?!\s?[^\s]+\')/u', - $identifier[$this->type] ?? '"$1"', - $statement - ); - - foreach ($map as $key => $value) { - if ($value[1] === PDO::PARAM_STR) { - $replace = $this->quote($value[0]); - } elseif ($value[1] === PDO::PARAM_NULL) { - $replace = 'NULL'; - } elseif ($value[1] === PDO::PARAM_LOB) { - $replace = '{LOB_DATA}'; - } else { - $replace = $value[0] . ''; - } - - $statement = str_replace($key, $replace, $statement); - } - - return $statement; - } - - /** - * Build a raw object. - * - * @param string $string The raw string. - * @param array $map The array of mapping data for the raw string. - * @return Medoo::raw - */ - public static function raw(string $string, array $map = []): Raw - { - $raw = new Raw(); - - $raw->map = $map; - $raw->value = $string; - - return $raw; - } - - /** - * Finds whether the object is raw. - * - * @param object $object - * @return bool - */ - protected function isRaw($object): bool - { - return $object instanceof Raw; - } - - /** - * Generate the actual query from the raw object. - * - * @param mixed $raw - * @param array $map - * @return string|null - */ - protected function buildRaw($raw, array &$map): ?string - { - if (!$this->isRaw($raw)) { - return null; - } - - $query = preg_replace_callback( - '/(([`\']).*?)?((FROM|TABLE|INTO|UPDATE|JOIN|TABLE IF EXISTS)\s*)?\<(([\p{L}_][\p{L}\p{N}@$#\-_]*)(\.[\p{L}_][\p{L}\p{N}@$#\-_]*)?)\>([^,]*?\2)?/u', - function ($matches) { - if (!empty($matches[2]) && isset($matches[8])) { - return $matches[0]; - } - - if (!empty($matches[4])) { - return $matches[1] . $matches[4] . ' ' . $this->tableQuote($matches[5]); - } - - return $matches[1] . $this->columnQuote($matches[5]); - }, - $raw->value - ); - - $rawMap = $raw->map; - - if (!empty($rawMap)) { - foreach ($rawMap as $key => $value) { - $map[$key] = $this->typeMap($value, gettype($value)); - } - } - - return $query; - } - - /** - * Quote a string for use in a query. - * - * @param string $string - * @return string - */ - public function quote(string $string): string - { - if ($this->type === 'mysql') { - return "'" . preg_replace(['/([\'"])/', '/(\\\\\\\")/'], ["\\\\\${1}", '\\\${1}'], $string) . "'"; - } - - return "'" . preg_replace('/\'/', '\'\'', $string) . "'"; - } - - /** - * Quote table name for use in a query. - * - * @param string $table - * @return string - */ - public function tableQuote(string $table): string - { - if (preg_match('/^[\p{L}_][\p{L}\p{N}@$#\-_]*$/u', $table)) { - return '"' . $this->prefix . $table . '"'; - } - - throw new InvalidArgumentException("Incorrect table name: {$table}."); - } - - /** - * Quote column name for use in a query. - * - * @param string $column - * @return string - */ - public function columnQuote(string $column): string - { - if (preg_match('/^[\p{L}_][\p{L}\p{N}@$#\-_]*(\.?[\p{L}_][\p{L}\p{N}@$#\-_]*)?$/u', $column)) { - return strpos($column, '.') !== false ? - '"' . $this->prefix . str_replace('.', '"."', $column) . '"' : - '"' . $column . '"'; - } - - throw new InvalidArgumentException("Incorrect column name: {$column}."); - } - - /** - * Mapping the type name as PDO data type. - * - * @param mixed $value - * @param string $type - * @return array - */ - protected function typeMap($value, string $type): array - { - $map = [ - 'NULL' => PDO::PARAM_NULL, - 'integer' => PDO::PARAM_INT, - 'double' => PDO::PARAM_STR, - 'boolean' => PDO::PARAM_BOOL, - 'string' => PDO::PARAM_STR, - 'object' => PDO::PARAM_STR, - 'resource' => PDO::PARAM_LOB - ]; - - if ($type === 'boolean') { - $value = ($value ? '1' : '0'); - } elseif ($type === 'NULL') { - $value = null; - } - - return [$value, $map[$type]]; - } - - /** - * Build the statement part for the column stack. - * - * @param array|string $columns - * @param array $map - * @param bool $root - * @param bool $isJoin - * @return string - */ - protected function columnPush(&$columns, array &$map, bool $root, bool $isJoin = false): string - { - if ($columns === '*') { - return $columns; - } - - $stack = []; - $hasDistinct = false; - - if (is_string($columns)) { - $columns = [$columns]; - } - - foreach ($columns as $key => $value) { - $isIntKey = is_int($key); - $isArrayValue = is_array($value); - - if (!$isIntKey && $isArrayValue && $root && count(array_keys($columns)) === 1) { - $stack[] = $this->columnQuote($key); - $stack[] = $this->columnPush($value, $map, false, $isJoin); - } elseif ($isArrayValue) { - $stack[] = $this->columnPush($value, $map, false, $isJoin); - } elseif (!$isIntKey && $raw = $this->buildRaw($value, $map)) { - preg_match('/(?[\p{L}_][\p{L}\p{N}@$#\-_\.]*)(\s*\[(?(String|Bool|Int|Number))\])?/u', $key, $match); - $stack[] = "{$raw} AS {$this->columnQuote($match['column'])}"; - } elseif ($isIntKey && is_string($value)) { - if ($isJoin && strpos($value, '*') !== false) { - throw new InvalidArgumentException('Cannot use table.* to select all columns while joining table.'); - } - - preg_match('/(?[\p{L}_][\p{L}\p{N}@$#\-_\.]*)(?:\s*\((?[\p{L}_][\p{L}\p{N}@$#\-_]*)\))?(?:\s*\[(?(?:String|Bool|Int|Number|Object|JSON))\])?/u', $value, $match); - - $columnString = ''; - - if (!empty($match['alias'])) { - $columnString = "{$this->columnQuote($match['column'])} AS {$this->columnQuote($match['alias'])}"; - $columns[$key] = $match['alias']; - - if (!empty($match['type'])) { - $columns[$key] .= ' [' . $match['type'] . ']'; - } - } else { - $columnString = $this->columnQuote($match['column']); - } - - if (!$hasDistinct && strpos($value, '@') === 0) { - $columnString = 'DISTINCT ' . $columnString; - $hasDistinct = true; - array_unshift($stack, $columnString); - - continue; - } - - $stack[] = $columnString; - } - } - - return implode(',', $stack); - } - - /** - * Implode the Where conditions. - * - * @param array $data - * @param array $map - * @param string $conjunctor - * @return string - */ - protected function dataImplode(array $data, array &$map, string $conjunctor): string - { - $stack = []; - - foreach ($data as $key => $value) { - $type = gettype($value); - - if ( - $type === 'array' && - preg_match("/^(AND|OR)(\s+#.*)?$/", $key, $relationMatch) - ) { - $stack[] = '(' . $this->dataImplode($value, $map, ' ' . $relationMatch[1]) . ')'; - continue; - } - - $mapKey = $this->mapKey(); - $isIndex = is_int($key); - - preg_match( - '/([\p{L}_][\p{L}\p{N}@$#\-_\.]*)(\[(?.*)\])?([\p{L}_][\p{L}\p{N}@$#\-_\.]*)?/u', - $isIndex ? $value : $key, - $match - ); - - $column = $this->columnQuote($match[1]); - $operator = $match['operator'] ?? null; - - if ($isIndex && isset($match[4]) && in_array($operator, ['>', '>=', '<', '<=', '=', '!='])) { - $stack[] = "${column} ${operator} " . $this->columnQuote($match[4]); - continue; - } - - if ($operator && $operator != '=') { - if (in_array($operator, ['>', '>=', '<', '<='])) { - $condition = "{$column} {$operator} "; - - if (is_numeric($value)) { - $condition .= $mapKey; - $map[$mapKey] = [$value, is_float($value) ? PDO::PARAM_STR : PDO::PARAM_INT]; - } elseif ($raw = $this->buildRaw($value, $map)) { - $condition .= $raw; - } else { - $condition .= $mapKey; - $map[$mapKey] = [$value, PDO::PARAM_STR]; - } - - $stack[] = $condition; - } elseif ($operator === '!') { - switch ($type) { - - case 'NULL': - $stack[] = $column . ' IS NOT NULL'; - break; - - case 'array': - $placeholders = []; - - foreach ($value as $index => $item) { - $stackKey = $mapKey . $index . '_i'; - $placeholders[] = $stackKey; - $map[$stackKey] = $this->typeMap($item, gettype($item)); - } - - $stack[] = $column . ' NOT IN (' . implode(', ', $placeholders) . ')'; - break; - - case 'object': - if ($raw = $this->buildRaw($value, $map)) { - $stack[] = "{$column} != {$raw}"; - } - break; - - case 'integer': - case 'double': - case 'boolean': - case 'string': - $stack[] = "{$column} != {$mapKey}"; - $map[$mapKey] = $this->typeMap($value, $type); - break; - } - } elseif ($operator === '~' || $operator === '!~') { - if ($type !== 'array') { - $value = [$value]; - } - - $connector = ' OR '; - $data = array_values($value); - - if (is_array($data[0])) { - if (isset($value['AND']) || isset($value['OR'])) { - $connector = ' ' . array_keys($value)[0] . ' '; - $value = $data[0]; - } - } - - $likeClauses = []; - - foreach ($value as $index => $item) { - $item = strval($item); - - if (!preg_match('/((?' || $operator === '><') { - if ($type === 'array') { - if ($operator === '><') { - $column .= ' NOT'; - } - - if ($this->isRaw($value[0]) && $this->isRaw($value[1])) { - $stack[] = "({$column} BETWEEN {$this->buildRaw($value[0], $map)} AND {$this->buildRaw($value[1], $map)})"; - } else { - $stack[] = "({$column} BETWEEN {$mapKey}a AND {$mapKey}b)"; - $dataType = (is_numeric($value[0]) && is_numeric($value[1])) ? PDO::PARAM_INT : PDO::PARAM_STR; - - $map[$mapKey . 'a'] = [$value[0], $dataType]; - $map[$mapKey . 'b'] = [$value[1], $dataType]; - } - } - } elseif ($operator === 'REGEXP') { - $stack[] = "{$column} REGEXP {$mapKey}"; - $map[$mapKey] = [$value, PDO::PARAM_STR]; - } else { - throw new InvalidArgumentException("Invalid operator [{$operator}] for column {$column} supplied."); - } - - continue; - } - - switch ($type) { - - case 'NULL': - $stack[] = $column . ' IS NULL'; - break; - - case 'array': - $placeholders = []; - - foreach ($value as $index => $item) { - $stackKey = $mapKey . $index . '_i'; - - $placeholders[] = $stackKey; - $map[$stackKey] = $this->typeMap($item, gettype($item)); - } - - $stack[] = $column . ' IN (' . implode(', ', $placeholders) . ')'; - break; - - case 'object': - if ($raw = $this->buildRaw($value, $map)) { - $stack[] = "{$column} = {$raw}"; - } - break; - - case 'integer': - case 'double': - case 'boolean': - case 'string': - $stack[] = "{$column} = {$mapKey}"; - $map[$mapKey] = $this->typeMap($value, $type); - break; - } - } - - return implode($conjunctor . ' ', $stack); - } - - /** - * Build the where clause. - * - * @param array|null $where - * @param array $map - * @return string - */ - protected function whereClause($where, array &$map): string - { - $clause = ''; - - if (is_array($where)) { - $conditions = array_diff_key($where, array_flip( - ['GROUP', 'ORDER', 'HAVING', 'LIMIT', 'LIKE', 'MATCH'] - )); - - if (!empty($conditions)) { - $clause = ' WHERE ' . $this->dataImplode($conditions, $map, ' AND'); - } - - if (isset($where['MATCH']) && $this->type === 'mysql') { - $match = $where['MATCH']; - - if (is_array($match) && isset($match['columns'], $match['keyword'])) { - $mode = ''; - - $options = [ - 'natural' => 'IN NATURAL LANGUAGE MODE', - 'natural+query' => 'IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION', - 'boolean' => 'IN BOOLEAN MODE', - 'query' => 'WITH QUERY EXPANSION' - ]; - - if (isset($match['mode'], $options[$match['mode']])) { - $mode = ' ' . $options[$match['mode']]; - } - - $columns = implode(', ', array_map([$this, 'columnQuote'], $match['columns'])); - $mapKey = $this->mapKey(); - $map[$mapKey] = [$match['keyword'], PDO::PARAM_STR]; - $clause .= ($clause !== '' ? ' AND ' : ' WHERE') . ' MATCH (' . $columns . ') AGAINST (' . $mapKey . $mode . ')'; - } - } - - if (isset($where['GROUP'])) { - $group = $where['GROUP']; - - if (is_array($group)) { - $stack = []; - - foreach ($group as $column => $value) { - $stack[] = $this->columnQuote($value); - } - - $clause .= ' GROUP BY ' . implode(',', $stack); - } elseif ($raw = $this->buildRaw($group, $map)) { - $clause .= ' GROUP BY ' . $raw; - } else { - $clause .= ' GROUP BY ' . $this->columnQuote($group); - } - } - - if (isset($where['HAVING'])) { - $having = $where['HAVING']; - - if ($raw = $this->buildRaw($having, $map)) { - $clause .= ' HAVING ' . $raw; - } else { - $clause .= ' HAVING ' . $this->dataImplode($having, $map, ' AND'); - } - } - - if (isset($where['ORDER'])) { - $order = $where['ORDER']; - - if (is_array($order)) { - $stack = []; - - foreach ($order as $column => $value) { - if (is_array($value)) { - $valueStack = []; - - foreach ($value as $item) { - $valueStack[] = is_int($item) ? $item : $this->quote($item); - } - - $valueString = implode(',', $valueStack); - $stack[] = "FIELD({$this->columnQuote($column)}, {$valueString})"; - } elseif ($value === 'ASC' || $value === 'DESC') { - $stack[] = $this->columnQuote($column) . ' ' . $value; - } elseif (is_int($column)) { - $stack[] = $this->columnQuote($value); - } - } - - $clause .= ' ORDER BY ' . implode(',', $stack); - } elseif ($raw = $this->buildRaw($order, $map)) { - $clause .= ' ORDER BY ' . $raw; - } else { - $clause .= ' ORDER BY ' . $this->columnQuote($order); - } - } - - if (isset($where['LIMIT'])) { - $limit = $where['LIMIT']; - - if (in_array($this->type, ['oracle', 'mssql'])) { - if ($this->type === 'mssql' && !isset($where['ORDER'])) { - $clause .= ' ORDER BY (SELECT 0)'; - } - - if (is_numeric($limit)) { - $limit = [0, $limit]; - } - - if ( - is_array($limit) && - is_numeric($limit[0]) && - is_numeric($limit[1]) - ) { - $clause .= " OFFSET {$limit[0]} ROWS FETCH NEXT {$limit[1]} ROWS ONLY"; - } - } else { - if (is_numeric($limit)) { - $clause .= ' LIMIT ' . $limit; - } elseif ( - is_array($limit) && - is_numeric($limit[0]) && - is_numeric($limit[1]) - ) { - $clause .= " LIMIT {$limit[1]} OFFSET {$limit[0]}"; - } - } - } - } elseif ($raw = $this->buildRaw($where, $map)) { - $clause .= ' ' . $raw; - } - - return $clause; - } - - /** - * Build statement for the select query. - * - * @param string $table - * @param array $map - * @param array|string $join - * @param array|string $columns - * @param array $where - * @param string $columnFn - * @return string - */ - protected function selectContext( - string $table, - array &$map, - $join, - &$columns = null, - $where = null, - $columnFn = null - ): string { - preg_match('/(?
[\p{L}_][\p{L}\p{N}@$#\-_]*)\s*\((?[\p{L}_][\p{L}\p{N}@$#\-_]*)\)/u', $table, $tableMatch); - - if (isset($tableMatch['table'], $tableMatch['alias'])) { - $table = $this->tableQuote($tableMatch['table']); - $tableAlias = $this->tableQuote($tableMatch['alias']); - $tableQuery = "{$table} AS {$tableAlias}"; - } else { - $table = $this->tableQuote($table); - $tableQuery = $table; - } - - $isJoin = $this->isJoin($join); - - if ($isJoin) { - $tableQuery .= ' ' . $this->buildJoin($tableAlias ?? $table, $join, $map); - } else { - if (is_null($columns)) { - if ( - !is_null($where) || - (is_array($join) && isset($columnFn)) - ) { - $where = $join; - $columns = null; - } else { - $where = null; - $columns = $join; - } - } else { - $where = $columns; - $columns = $join; - } - } - - if (isset($columnFn)) { - if ($columnFn === 1) { - $column = '1'; - - if (is_null($where)) { - $where = $columns; - } - } elseif ($raw = $this->buildRaw($columnFn, $map)) { - $column = $raw; - } else { - if (empty($columns) || $this->isRaw($columns)) { - $columns = '*'; - $where = $join; - } - - $column = $columnFn . '(' . $this->columnPush($columns, $map, true) . ')'; - } - } else { - $column = $this->columnPush($columns, $map, true, $isJoin); - } - - return 'SELECT ' . $column . ' FROM ' . $tableQuery . $this->whereClause($where, $map); - } - - /** - * Determine the array with join syntax. - * - * @param mixed $join - * @return bool - */ - protected function isJoin($join): bool - { - if (!is_array($join)) { - return false; - } - - $keys = array_keys($join); - - if ( - isset($keys[0]) && - is_string($keys[0]) && - strpos($keys[0], '[') === 0 - ) { - return true; - } - - return false; - } - - /** - * Build the join statement. - * - * @param string $table - * @param array $join - * @param array $map - * @return string - */ - protected function buildJoin(string $table, array $join, array &$map): string - { - $tableJoin = []; - $type = [ - '>' => 'LEFT', - '<' => 'RIGHT', - '<>' => 'FULL', - '><' => 'INNER' - ]; - - foreach ($join as $subtable => $relation) { - preg_match('/(\[(?\<\>?|\>\[\p{L}_][\p{L}\p{N}@$#\-_]*)\s?(\((?[\p{L}_][\p{L}\p{N}@$#\-_]*)\))?/u', $subtable, $match); - - if ($match['join'] === '' || $match['table'] === '') { - continue; - } - - if (is_string($relation)) { - $relation = 'USING ("' . $relation . '")'; - } elseif (is_array($relation)) { - // For ['column1', 'column2'] - if (isset($relation[0])) { - $relation = 'USING ("' . implode('", "', $relation) . '")'; - } else { - $joins = []; - - foreach ($relation as $key => $value) { - if ($key === 'AND' && is_array($value)) { - $joins[] = $this->dataImplode($value, $map, ' AND'); - continue; - } - - $joins[] = ( - strpos($key, '.') > 0 ? - // For ['tableB.column' => 'column'] - $this->columnQuote($key) : - - // For ['column1' => 'column2'] - $table . '.' . $this->columnQuote($key) - ) . - ' = ' . - $this->tableQuote($match['alias'] ?? $match['table']) . '.' . $this->columnQuote($value); - } - - $relation = 'ON ' . implode(' AND ', $joins); - } - } elseif ($raw = $this->buildRaw($relation, $map)) { - $relation = $raw; - } - - $tableName = $this->tableQuote($match['table']); - - if (isset($match['alias'])) { - $tableName .= ' AS ' . $this->tableQuote($match['alias']); - } - - $tableJoin[] = $type[$match['join']] . " JOIN ${tableName} ${relation}"; - } - - return implode(' ', $tableJoin); - } - - /** - * Mapping columns for the stack. - * - * @param array|string $columns - * @param array $stack - * @param bool $root - * @return array - */ - protected function columnMap($columns, array &$stack, bool $root): array - { - if ($columns === '*') { - return $stack; - } - - foreach ($columns as $key => $value) { - if (is_int($key)) { - preg_match('/([\p{L}_][\p{L}\p{N}@$#\-_]*\.)?(?[\p{L}_][\p{L}\p{N}@$#\-_]*)(?:\s*\((?[\p{L}_][\p{L}\p{N}@$#\-_]*)\))?(?:\s*\[(?(?:String|Bool|Int|Number|Object|JSON))\])?/u', $value, $keyMatch); - - $columnKey = !empty($keyMatch['alias']) ? - $keyMatch['alias'] : - $keyMatch['column']; - - $stack[$value] = isset($keyMatch['type']) ? - [$columnKey, $keyMatch['type']] : - [$columnKey, 'String']; - } elseif ($this->isRaw($value)) { - preg_match('/([\p{L}_][\p{L}\p{N}@$#\-_]*\.)?(?[\p{L}_][\p{L}\p{N}@$#\-_]*)(\s*\[(?(String|Bool|Int|Number))\])?/u', $key, $keyMatch); - $columnKey = $keyMatch['column']; - - $stack[$key] = isset($keyMatch['type']) ? - [$columnKey, $keyMatch['type']] : - [$columnKey, 'String']; - } elseif (!is_int($key) && is_array($value)) { - if ($root && count(array_keys($columns)) === 1) { - $stack[$key] = [$key, 'String']; - } - - $this->columnMap($value, $stack, false); - } - } - - return $stack; - } - - /** - * Mapping the data from the table. - * - * @param array $data - * @param array $columns - * @param array $columnMap - * @param array $stack - * @param bool $root - * @param array $result - * @codeCoverageIgnore - * @return void - */ - protected function dataMap( - array $data, - array $columns, - array $columnMap, - array &$stack, - bool $root, - array &$result = null - ): void { - if ($root) { - $columnsKey = array_keys($columns); - - if (count($columnsKey) === 1 && is_array($columns[$columnsKey[0]])) { - $indexKey = array_keys($columns)[0]; - $dataKey = preg_replace("/^[\p{L}_][\p{L}\p{N}@$#\-_]*\./u", '', $indexKey); - $currentStack = []; - - foreach ($data as $item) { - $this->dataMap($data, $columns[$indexKey], $columnMap, $currentStack, false, $result); - $index = $data[$dataKey]; - - if (isset($result)) { - $result[$index] = $currentStack; - } else { - $stack[$index] = $currentStack; - } - } - } else { - $currentStack = []; - $this->dataMap($data, $columns, $columnMap, $currentStack, false, $result); - - if (isset($result)) { - $result[] = $currentStack; - } else { - $stack = $currentStack; - } - } - - return; - } - - foreach ($columns as $key => $value) { - $isRaw = $this->isRaw($value); - - if (is_int($key) || $isRaw) { - $map = $columnMap[$isRaw ? $key : $value]; - $columnKey = $map[0]; - $item = $data[$columnKey]; - - if (isset($map[1])) { - if ($isRaw && in_array($map[1], ['Object', 'JSON'])) { - continue; - } - - if (is_null($item)) { - $stack[$columnKey] = null; - continue; - } - - switch ($map[1]) { - - case 'Number': - $stack[$columnKey] = (float) $item; - break; - - case 'Int': - $stack[$columnKey] = (int) $item; - break; - - case 'Bool': - $stack[$columnKey] = (bool) $item; - break; - - case 'Object': - $stack[$columnKey] = unserialize($item); - break; - - case 'JSON': - $stack[$columnKey] = json_decode($item, true); - break; - - case 'String': - $stack[$columnKey] = $item; - break; - } - } else { - $stack[$columnKey] = $item; - } - } else { - $currentStack = []; - $this->dataMap($data, $value, $columnMap, $currentStack, false, $result); - - $stack[$key] = $currentStack; - } - } - } - - /** - * Build and execute returning query. - * - * @param string $query - * @param array $map - * @param array $data - * @return \PDOStatement|null - */ - private function returningQuery($query, &$map, &$data): ?PDOStatement - { - $returnColumns = array_map( - function ($value) { - return $value[0]; - }, - $data - ); - - $query .= ' RETURNING ' . - implode(', ', array_map([$this, 'columnQuote'], $returnColumns)) . - ' INTO ' . - implode(', ', array_keys($data)); - - return $this->exec($query, $map, function ($statement) use (&$data) { - // @codeCoverageIgnoreStart - foreach ($data as $key => $return) { - if (isset($return[3])) { - $statement->bindParam($key, $data[$key][1], $return[2], $return[3]); - } else { - $statement->bindParam($key, $data[$key][1], $return[2]); - } - } - // @codeCoverageIgnoreEnd - }); - } - - /** - * Create a table. - * - * @param string $table - * @param array $columns Columns definition. - * @param array $options Additional table options for creating a table. - * @return \PDOStatement|null - */ - public function create(string $table, $columns, $options = null): ?PDOStatement - { - $stack = []; - $tableOption = ''; - $tableName = $this->tableQuote($table); - - foreach ($columns as $name => $definition) { - if (is_int($name)) { - $stack[] = preg_replace('/\<([\p{L}_][\p{L}\p{N}@$#\-_]*)\>/u', '"$1"', $definition); - } elseif (is_array($definition)) { - $stack[] = $this->columnQuote($name) . ' ' . implode(' ', $definition); - } elseif (is_string($definition)) { - $stack[] = $this->columnQuote($name) . ' ' . $definition; - } - } - - if (is_array($options)) { - $optionStack = []; - - foreach ($options as $key => $value) { - if (is_string($value) || is_int($value)) { - $optionStack[] = "{$key} = {$value}"; - } - } - - $tableOption = ' ' . implode(', ', $optionStack); - } elseif (is_string($options)) { - $tableOption = ' ' . $options; - } - - $command = 'CREATE TABLE'; - - if (in_array($this->type, ['mysql', 'pgsql', 'sqlite'])) { - $command .= ' IF NOT EXISTS'; - } - - return $this->exec("{$command} {$tableName} (" . implode(', ', $stack) . "){$tableOption}"); - } - - /** - * Drop a table. - * - * @param string $table - * @return \PDOStatement|null - */ - public function drop(string $table): ?PDOStatement - { - return $this->exec('DROP TABLE IF EXISTS ' . $this->tableQuote($table)); - } - - /** - * Select data from the table. - * - * @param string $table - * @param array $join - * @param array|string $columns - * @param array $where - * @return array|null - */ - public function select(string $table, $join, $columns = null, $where = null): ?array - { - $map = []; - $result = []; - $columnMap = []; - - $args = func_get_args(); - $lastArgs = $args[array_key_last($args)]; - $callback = is_callable($lastArgs) ? $lastArgs : null; - - $where = is_callable($where) ? null : $where; - $columns = is_callable($columns) ? null : $columns; - - $column = $where === null ? $join : $columns; - $isSingle = (is_string($column) && $column !== '*'); - - $statement = $this->exec($this->selectContext($table, $map, $join, $columns, $where), $map); - - $this->columnMap($columns, $columnMap, true); - - if (!$this->statement) { - return $result; - } - - // @codeCoverageIgnoreStart - if ($columns === '*') { - if (isset($callback)) { - while ($data = $statement->fetch(PDO::FETCH_ASSOC)) { - $callback($data); - } - - return null; - } - - return $statement->fetchAll(PDO::FETCH_ASSOC); - } - - while ($data = $statement->fetch(PDO::FETCH_ASSOC)) { - $currentStack = []; - - if (isset($callback)) { - $this->dataMap($data, $columns, $columnMap, $currentStack, true); - - $callback( - $isSingle ? - $currentStack[$columnMap[$column][0]] : - $currentStack - ); - } else { - $this->dataMap($data, $columns, $columnMap, $currentStack, true, $result); - } - } - - if (isset($callback)) { - return null; - } - - if ($isSingle) { - $singleResult = []; - $resultKey = $columnMap[$column][0]; - - foreach ($result as $item) { - $singleResult[] = $item[$resultKey]; - } - - return $singleResult; - } - - return $result; - } - // @codeCoverageIgnoreEnd - - /** - * Insert one or more records into the table. - * - * @param string $table - * @param array $values - * @param string $primaryKey - * @return \PDOStatement|null - */ - public function insert(string $table, array $values, string $primaryKey = null): ?PDOStatement - { - $stack = []; - $columns = []; - $fields = []; - $map = []; - $returnings = []; - - if (!isset($values[0])) { - $values = [$values]; - } - - foreach ($values as $data) { - foreach ($data as $key => $value) { - $columns[] = $key; - } - } - - $columns = array_unique($columns); - - foreach ($values as $data) { - $values = []; - - foreach ($columns as $key) { - $value = $data[$key]; - $type = gettype($value); - - if ($this->type === 'oracle' && $type === 'resource') { - $values[] = 'EMPTY_BLOB()'; - $returnings[$this->mapKey()] = [$key, $value, PDO::PARAM_LOB]; - continue; - } - - if ($raw = $this->buildRaw($data[$key], $map)) { - $values[] = $raw; - continue; - } - - $mapKey = $this->mapKey(); - $values[] = $mapKey; - - switch ($type) { - - case 'array': - $map[$mapKey] = [ - strpos($key, '[JSON]') === strlen($key) - 6 ? - json_encode($value) : - serialize($value), - PDO::PARAM_STR - ]; - break; - - case 'object': - $value = serialize($value); - break; - - case 'NULL': - case 'resource': - case 'boolean': - case 'integer': - case 'double': - case 'string': - $map[$mapKey] = $this->typeMap($value, $type); - break; - } - } - - $stack[] = '(' . implode(', ', $values) . ')'; - } - - foreach ($columns as $key) { - $fields[] = $this->columnQuote(preg_replace("/(\s*\[JSON\]$)/i", '', $key)); - } - - $query = 'INSERT INTO ' . $this->tableQuote($table) . ' (' . implode(', ', $fields) . ') VALUES ' . implode(', ', $stack); - - if ( - $this->type === 'oracle' && (!empty($returnings) || isset($primaryKey)) - ) { - if ($primaryKey) { - $returnings[':RETURNID'] = [$primaryKey, '', PDO::PARAM_INT, 8]; - } - - $statement = $this->returningQuery($query, $map, $returnings); - - if ($primaryKey) { - $this->returnId = $returnings[':RETURNID'][1]; - } - - return $statement; - } - - return $this->exec($query, $map); - } - - /** - * Modify data from the table. - * - * @param string $table - * @param array $data - * @param array $where - * @return \PDOStatement|null - */ - public function update(string $table, $data, $where = null): ?PDOStatement - { - $fields = []; - $map = []; - $returnings = []; - - foreach ($data as $key => $value) { - $column = $this->columnQuote(preg_replace("/(\s*\[(JSON|\+|\-|\*|\/)\]$)/", '', $key)); - $type = gettype($value); - - if ($this->type === 'oracle' && $type === 'resource') { - $fields[] = "{$column} = EMPTY_BLOB()"; - $returnings[$this->mapKey()] = [$key, $value, PDO::PARAM_LOB]; - continue; - } - - if ($raw = $this->buildRaw($value, $map)) { - $fields[] = "{$column} = {$raw}"; - continue; - } - - preg_match('/(?[\p{L}_][\p{L}\p{N}@$#\-_]*)(\[(?\+|\-|\*|\/)\])?/u', $key, $match); - - if (isset($match['operator'])) { - if (is_numeric($value)) { - $fields[] = "{$column} = {$column} {$match['operator']} {$value}"; - } - } else { - $mapKey = $this->mapKey(); - $fields[] = "{$column} = {$mapKey}"; - - switch ($type) { - - case 'array': - $map[$mapKey] = [ - strpos($key, '[JSON]') === strlen($key) - 6 ? - json_encode($value) : - serialize($value), - PDO::PARAM_STR - ]; - break; - - case 'object': - $value = serialize($value); - - break; - case 'NULL': - case 'resource': - case 'boolean': - case 'integer': - case 'double': - case 'string': - $map[$mapKey] = $this->typeMap($value, $type); - break; - } - } - } - - $query = 'UPDATE ' . $this->tableQuote($table) . ' SET ' . implode(', ', $fields) . $this->whereClause($where, $map); - - if ($this->type === 'oracle' && !empty($returnings)) { - return $this->returningQuery($query, $map, $returnings); - } - - return $this->exec($query, $map); - } - - /** - * Delete data from the table. - * - * @param string $table - * @param array|Raw $where - * @return \PDOStatement|null - */ - public function delete(string $table, $where): ?PDOStatement - { - $map = []; - - return $this->exec('DELETE FROM ' . $this->tableQuote($table) . $this->whereClause($where, $map), $map); - } - - /** - * Replace old data with a new one. - * - * @param string $table - * @param array $columns - * @param array $where - * @return \PDOStatement|null - */ - public function replace(string $table, array $columns, $where = null): ?PDOStatement - { - $map = []; - $stack = []; - - foreach ($columns as $column => $replacements) { - if (is_array($replacements)) { - foreach ($replacements as $old => $new) { - $mapKey = $this->mapKey(); - $columnName = $this->columnQuote($column); - $stack[] = "{$columnName} = REPLACE({$columnName}, {$mapKey}a, {$mapKey}b)"; - - $map[$mapKey . 'a'] = [$old, PDO::PARAM_STR]; - $map[$mapKey . 'b'] = [$new, PDO::PARAM_STR]; - } - } - } - - if (empty($stack)) { - throw new InvalidArgumentException('Invalid columns supplied.'); - } - - return $this->exec('UPDATE ' . $this->tableQuote($table) . ' SET ' . implode(', ', $stack) . $this->whereClause($where, $map), $map); - } - - /** - * Get only one record from the table. - * - * @param string $table - * @param array $join - * @param array|string $columns - * @param array $where - * @return mixed - */ - public function get(string $table, $join = null, $columns = null, $where = null) - { - $map = []; - $result = []; - $columnMap = []; - $currentStack = []; - - if ($where === null) { - if ($this->isJoin($join)) { - $where['LIMIT'] = 1; - } else { - $columns['LIMIT'] = 1; - } - - $column = $join; - } else { - $column = $columns; - $where['LIMIT'] = 1; - } - - $isSingle = (is_string($column) && $column !== '*'); - $query = $this->exec($this->selectContext($table, $map, $join, $columns, $where), $map); - - if (!$this->statement) { - return false; - } - - // @codeCoverageIgnoreStart - $data = $query->fetchAll(PDO::FETCH_ASSOC); - - if (isset($data[0])) { - if ($column === '*') { - return $data[0]; - } - - $this->columnMap($columns, $columnMap, true); - $this->dataMap($data[0], $columns, $columnMap, $currentStack, true, $result); - - if ($isSingle) { - return $result[0][$columnMap[$column][0]]; - } - - return $result[0]; - } - } - // @codeCoverageIgnoreEnd - - /** - * Determine whether the target data existed from the table. - * - * @param string $table - * @param array $join - * @param array $where - * @return bool - */ - public function has(string $table, $join, $where = null): bool - { - $map = []; - $column = null; - - $query = $this->exec( - $this->type === 'mssql' ? - $this->selectContext($table, $map, $join, $column, $where, Medoo::raw('TOP 1 1')) : - 'SELECT EXISTS(' . $this->selectContext($table, $map, $join, $column, $where, 1) . ')', - $map - ); - - if (!$this->statement) { - return false; - } - - // @codeCoverageIgnoreStart - $result = $query->fetchColumn(); - - return $result === '1' || $result === 1 || $result === true; - } - // @codeCoverageIgnoreEnd - - /** - * Randomly fetch data from the table. - * - * @param string $table - * @param array $join - * @param array|string $columns - * @param array $where - * @return array - */ - public function rand(string $table, $join = null, $columns = null, $where = null): array - { - $orderRaw = $this->raw( - $this->type === 'mysql' ? 'RAND()' - : ($this->type === 'mssql' ? 'NEWID()' - : 'RANDOM()') - ); - - if ($where === null) { - if ($this->isJoin($join)) { - $where['ORDER'] = $orderRaw; - } else { - $columns['ORDER'] = $orderRaw; - } - } else { - $where['ORDER'] = $orderRaw; - } - - return $this->select($table, $join, $columns, $where); - } - - /** - * Build for the aggregate function. - * - * @param string $type - * @param string $table - * @param array $join - * @param string $column - * @param array $where - * @return string|null - */ - private function aggregate(string $type, string $table, $join = null, $column = null, $where = null): ?string - { - $map = []; - - $query = $this->exec($this->selectContext($table, $map, $join, $column, $where, $type), $map); - - if (!$this->statement) { - return null; - } - - // @codeCoverageIgnoreStart - return (string) $query->fetchColumn(); - } - // @codeCoverageIgnoreEnd - - /** - * Count the number of rows from the table. - * - * @param string $table - * @param array $join - * @param string $column - * @param array $where - * @return int|null - */ - public function count(string $table, $join = null, $column = null, $where = null): ?int - { - return (int) $this->aggregate('COUNT', $table, $join, $column, $where); - } - - /** - * Calculate the average value of the column. - * - * @param string $table - * @param array $join - * @param string $column - * @param array $where - * @return string|null - */ - public function avg(string $table, $join, $column = null, $where = null): ?string - { - return $this->aggregate('AVG', $table, $join, $column, $where); - } - - /** - * Get the maximum value of the column. - * - * @param string $table - * @param array $join - * @param string $column - * @param array $where - * @return string|null - */ - public function max(string $table, $join, $column = null, $where = null): ?string - { - return $this->aggregate('MAX', $table, $join, $column, $where); - } - - /** - * Get the minimum value of the column. - * - * @param string $table - * @param array $join - * @param string $column - * @param array $where - * @return string|null - */ - public function min(string $table, $join, $column = null, $where = null): ?string - { - return $this->aggregate('MIN', $table, $join, $column, $where); - } - - /** - * Calculate the total value of the column. - * - * @param string $table - * @param array $join - * @param string $column - * @param array $where - * @return string|null - */ - public function sum(string $table, $join, $column = null, $where = null): ?string - { - return $this->aggregate('SUM', $table, $join, $column, $where); - } - - /** - * Start a transaction. - * - * @param callable $actions - * @codeCoverageIgnore - * @return void - */ - public function action(callable $actions): void - { - if (is_callable($actions)) { - $this->pdo->beginTransaction(); - - try { - $result = $actions($this); - - if ($result === false) { - $this->pdo->rollBack(); - } else { - $this->pdo->commit(); - } - } catch (Exception $e) { - $this->pdo->rollBack(); - throw $e; - } - } - } - - /** - * Return the ID for the last inserted row. - * - * @param string $name - * @codeCoverageIgnore - * @return string|null - */ - public function id(string $name = null): ?string - { - $type = $this->type; - - if ($type === 'oracle') { - return $this->returnId; - } elseif ($type === 'pgsql') { - $id = $this->pdo->query('SELECT LASTVAL()')->fetchColumn(); - - return (string) $id ?: null; - } - - return $this->pdo->lastInsertId($name); - } - - /** - * Enable debug mode and output readable statement string. - * - * @codeCoverageIgnore - * @return Medoo - */ - public function debug(): self - { - $this->debugMode = true; - - return $this; - } - - /** - * Enable debug logging mode. - * - * @codeCoverageIgnore - * @return void - */ - public function beginDebug(): void - { - $this->debugMode = true; - $this->debugLogging = true; - } - - /** - * Disable debug logging and return all readable statements. - * - * @codeCoverageIgnore - * @return void - */ - public function debugLog(): array - { - $this->debugMode = false; - $this->debugLogging = false; - - return $this->debugLogs; - } - - /** - * Return the last performed statement. - * - * @codeCoverageIgnore - * @return string|null - */ - public function last(): ?string - { - if (empty($this->logs)) { - return null; - } - - $log = $this->logs[array_key_last($this->logs)]; - - return $this->generate($log[0], $log[1]); - } - - /** - * Return all executed statements. - * - * @codeCoverageIgnore - * @return string[] - */ - public function log(): array - { - return array_map( - function ($log) { - return $this->generate($log[0], $log[1]); - }, - $this->logs - ); - } - - /** - * Get information about the database connection. - * - * @codeCoverageIgnore - * @return array - */ - public function info(): array - { - $output = [ - 'server' => 'SERVER_INFO', - 'driver' => 'DRIVER_NAME', - 'client' => 'CLIENT_VERSION', - 'version' => 'SERVER_VERSION', - 'connection' => 'CONNECTION_STATUS' - ]; - - foreach ($output as $key => $value) { - $output[$key] = @$this->pdo->getAttribute(constant('PDO::ATTR_' . $value)); - } - - $output['dsn'] = $this->dsn; - - return $output; - } -} diff --git a/include/thirdparty/db/tests/AggregateTest.php b/include/thirdparty/db/tests/AggregateTest.php new file mode 100644 index 0000000..fcb3e00 --- /dev/null +++ b/include/thirdparty/db/tests/AggregateTest.php @@ -0,0 +1,117 @@ +setType($type); + + $this->database->count("account", [ + "gender" => "female" + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::max() + * @covers ::aggregate() + * @covers ::selectContext() + * @dataProvider typesProvider + */ + public function testMax($type) + { + $this->setType($type); + + $this->database->max("account", "age"); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::min() + * @covers ::aggregate() + * @covers ::selectContext() + * @dataProvider typesProvider + */ + public function testMin($type) + { + $this->setType($type); + + $this->database->min("account", "age"); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::avg() + * @covers ::aggregate() + * @covers ::selectContext() + * @dataProvider typesProvider + */ + public function testAvg($type) + { + $this->setType($type); + + $this->database->avg("account", "age"); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::sum() + * @covers ::aggregate() + * @covers ::selectContext() + * @dataProvider typesProvider + */ + public function testSum($type) + { + $this->setType($type); + + $this->database->sum("account", "money"); + + $this->assertQuery( + <<database->queryString + ); + } +} diff --git a/include/thirdparty/db/tests/CreateTest.php b/include/thirdparty/db/tests/CreateTest.php new file mode 100644 index 0000000..b5c1d28 --- /dev/null +++ b/include/thirdparty/db/tests/CreateTest.php @@ -0,0 +1,142 @@ +setType($type); + + $this->database->create("account", [ + "id" => [ + "INT", + "NOT NULL", + "AUTO_INCREMENT" + ], + "email" => [ + "VARCHAR(70)", + "NOT NULL", + "UNIQUE" + ], + "PRIMARY KEY ()" + ], [ + "AUTO_INCREMENT" => 200 + ]); + + $this->assertQuery( + [ + 'default' => << << <<database->queryString + ); + } + + /** + * @covers ::create() + * @dataProvider typesProvider + */ + public function testCreateWithStringDefinition($type) + { + $this->setType($type); + + $this->database->create("account", [ + "id" => "INT NOT NULL AUTO_INCREMENT", + "email" => "VARCHAR(70) NOT NULL UNIQUE" + ]); + + $this->assertQuery( + [ + 'default' => << << <<database->queryString + ); + } + + /** + * @covers ::create() + * @dataProvider typesProvider + */ + public function testCreateWithSingleOption($type) + { + $this->setType($type); + + $this->database->create("account", [ + "id" => [ + "INT", + "NOT NULL", + "AUTO_INCREMENT" + ], + "email" => [ + "VARCHAR(70)", + "NOT NULL", + "UNIQUE" + ] + ], "TABLESPACE tablespace_name"); + + $this->assertQuery( + [ + 'default' => << << <<database->queryString + ); + } +} diff --git a/include/thirdparty/db/tests/DeleteTest.php b/include/thirdparty/db/tests/DeleteTest.php new file mode 100644 index 0000000..72cc8b1 --- /dev/null +++ b/include/thirdparty/db/tests/DeleteTest.php @@ -0,0 +1,59 @@ +setType($type); + + $this->database->delete("account", [ + "AND" => [ + "type" => "business", + "age[<]" => 18 + ] + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::delete() + * @dataProvider typesProvider + */ + public function testDeleteRaw($type) + { + $this->setType($type); + + $whereClause = Medoo::raw("WHERE ( = :type AND < :age)", [ + ':type' => 'business', + ':age' => 18, + ]); + + $this->database->delete("account", $whereClause); + + $this->assertQuery( + <<database->queryString + ); + } +} diff --git a/include/thirdparty/db/tests/DropTest.php b/include/thirdparty/db/tests/DropTest.php new file mode 100644 index 0000000..95c3fd0 --- /dev/null +++ b/include/thirdparty/db/tests/DropTest.php @@ -0,0 +1,51 @@ +setType($type); + + $this->database->drop("account"); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::drop() + */ + public function testDropWithPrefix() + { + $database = new Medoo([ + 'testMode' => true, + 'prefix' => 'PREFIX_' + ]); + + $database->type = "sqlite"; + + $database->drop("account"); + + $this->assertQuery( + <<queryString + ); + } +} diff --git a/include/thirdparty/db/tests/GetTest.php b/include/thirdparty/db/tests/GetTest.php new file mode 100644 index 0000000..4914001 --- /dev/null +++ b/include/thirdparty/db/tests/GetTest.php @@ -0,0 +1,163 @@ +setType($type); + + $this->database->get("account", "email", [ + "user_id" => 1234 + ]); + + $this->assertQuery([ + 'default' => << << <<database->queryString); + } + + /** + * @covers ::get() + * @dataProvider typesProvider + */ + public function testGetWithColumns($type) + { + $this->setType($type); + + $this->database->get("account", [ + "email", + "location" + ], [ + "user_id" => 1234 + ]); + + $this->assertQuery([ + 'default' => << << <<database->queryString); + } + + /** + * @covers ::get() + * @dataProvider typesProvider + */ + public function testGetWithJoin($type) + { + $this->setType($type); + + $this->database->get("post", [ + "[>]account" => "user_id" + ], [ + "post.content", + "account.user_name" + ]); + + $this->assertQuery([ + 'default' => << << <<database->queryString); + } + + /** + * @covers ::get() + * @dataProvider typesProvider + */ + public function testGetWithJoinAndWhere($type) + { + $this->setType($type); + + $this->database->get("post", [ + "[>]account" => "user_id" + ], [ + "post.content", + "account.user_name" + ], [ + 'account.age[>]' => 18 + ]); + + $this->assertQuery([ + 'default' => << 18 + LIMIT 1 + EOD, + 'mssql' => << 18 + ORDER BY (SELECT 0) + OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY + EOD, + 'oracle' => << 18 + OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY + EOD, + ], $this->database->queryString); + } +} diff --git a/include/thirdparty/db/tests/HasTest.php b/include/thirdparty/db/tests/HasTest.php new file mode 100644 index 0000000..6b88f6f --- /dev/null +++ b/include/thirdparty/db/tests/HasTest.php @@ -0,0 +1,32 @@ +setType($type); + + $this->database->has("account", [ + "user_name" => "foo" + ]); + + $this->assertQuery([ + 'default' => << <<database->queryString); + } +} diff --git a/include/thirdparty/db/tests/InsertTest.php b/include/thirdparty/db/tests/InsertTest.php new file mode 100644 index 0000000..9f8cda5 --- /dev/null +++ b/include/thirdparty/db/tests/InsertTest.php @@ -0,0 +1,242 @@ +setType($type); + + $this->database->insert("account", [ + "user_name" => "foo", + "email" => "foo@bar.com" + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::insert() + * @covers ::typeMap() + * @dataProvider typesProvider + */ + public function testInsertWithArray($type) + { + $this->setType($type); + + $this->database->insert("account", [ + "user_name" => "foo", + "lang" => ["en", "fr"] + ]); + + $this->assertQuery([ + 'default' => << <<database->queryString); + } + + /** + * @covers ::insert() + * @covers ::typeMap() + * @dataProvider typesProvider + */ + public function testInsertWithJSON($type) + { + $this->setType($type); + + $this->database->insert("account", [ + "user_name" => "foo", + "lang [JSON]" => ["en", "fr"] + ]); + + $this->assertQuery([ + 'default' => << <<database->queryString); + } + + /** + * @covers ::insert() + * @dataProvider typesProvider + */ + public function testInsertWithRaw($type) + { + $this->setType($type); + + $this->database->insert("account", [ + "user_name" => Medoo::raw("UUID()") + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::insert() + * @covers ::typeMap() + * @dataProvider typesProvider + */ + public function testInsertWithNull($type) + { + $this->setType($type); + + $this->database->insert("account", [ + "location" => null + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::insert() + * @covers ::typeMap() + * @dataProvider typesProvider + */ + public function testInsertWithObject($type) + { + $this->setType($type); + + $objectData = new Foo(); + + $this->database->insert("account", [ + "object" => $objectData + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::insert() + * @dataProvider typesProvider + */ + public function testMultiInsert($type) + { + $this->setType($type); + + $this->database->insert("account", [ + [ + "user_name" => "foo", + "email" => "foo@bar.com" + ], + [ + "user_name" => "bar", + "email" => "bar@foo.com" + ] + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + public function testOracleWithPrimaryKeyInsert() + { + $this->setType("oracle"); + + $this->database->insert("ACCOUNT", [ + "NAME" => "foo", + "EMAIL" => "foo@bar.com" + ], "ID"); + + $this->assertQuery( + <<database->queryString + ); + } + + public function testOracleWithLOBsInsert() + { + $this->setType("oracle"); + + $fp = fopen('README.md', 'r'); + + $this->database->insert("ACCOUNT", [ + "NAME" => "foo", + "DATA" => $fp + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + public function testOracleWithLOBsAndIdInsert() + { + $this->setType("oracle"); + + $fp = fopen('README.md', 'r'); + + $this->database->insert("ACCOUNT", [ + "NAME" => "foo", + "DATA" => $fp + ], "ID"); + + $this->assertQuery( + <<database->queryString + ); + } +} diff --git a/include/thirdparty/db/tests/MedooTestCase.php b/include/thirdparty/db/tests/MedooTestCase.php new file mode 100644 index 0000000..2554985 --- /dev/null +++ b/include/thirdparty/db/tests/MedooTestCase.php @@ -0,0 +1,82 @@ +database = new Medoo([ + 'testMode' => true + ]); + } + + public function typesProvider(): array + { + return [ + 'MySQL' => ['mysql'], + 'MSSQL' => ['mssql'], + 'SQLite' => ['sqlite'], + 'PostgreSQL' => ['pgsql'], + 'Oracle' => ['oracle'] + ]; + } + + public function setType($type): void + { + $this->database->setupType($type); + + if ($type === 'oracle') { + $this->tableAliasConnector = ' '; + } elseif ($type === 'mysql') { + $this->quotePattern = '`$1`'; + } elseif ($type === 'mssql') { + $this->quotePattern = '[$1]'; + } + } + + public function expectedQuery($expected): string + { + $result = preg_replace( + '/(?!\'[^\s]+\s?)"([\p{L}_][\p{L}\p{N}@$#\-_]*)"(?!\s?[^\s]+\')/u', + $this->quotePattern, + str_replace("\n", " ", $expected) + ); + + return str_replace( + ' @AS ', + $this->tableAliasConnector, + $result + ); + } + + public function assertQuery($expected, $query): void + { + if (is_array($expected)) { + $this->assertEquals( + $this->expectedQuery($expected[$this->database->type] ?? $expected['default']), + $query + ); + } else { + $this->assertEquals($this->expectedQuery($expected), $query); + } + } +} + +class Foo +{ + public $bar = "cat"; + + public function __wakeup() + { + $this->bar = "dog"; + } +} diff --git a/include/thirdparty/db/tests/QueryTest.php b/include/thirdparty/db/tests/QueryTest.php new file mode 100644 index 0000000..b5ca5eb --- /dev/null +++ b/include/thirdparty/db/tests/QueryTest.php @@ -0,0 +1,152 @@ +setType($type); + + $this->database->query("SELECT , FROM WHERE != 100"); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::query() + * @covers ::isRaw() + * @covers ::buildRaw() + */ + public function testQueryWithPrefix() + { + $database = new Medoo([ + 'testMode' => true, + 'prefix' => 'PREFIX_' + ]); + + $database->query("SELECT FROM "); + + $this->assertQuery( + <<queryString + ); + } + + /** + * @covers ::query() + * @covers ::isRaw() + * @covers ::buildRaw() + */ + public function testQueryTableWithPrefix() + { + $database = new Medoo([ + 'testMode' => true, + 'prefix' => 'PREFIX_' + ]); + + $database->query("DROP TABLE IF EXISTS "); + + $this->assertQuery( + <<queryString + ); + } + + /** + * @covers ::query() + * @covers ::isRaw() + * @covers ::buildRaw() + */ + public function testQueryShowTableWithPrefix() + { + $database = new Medoo([ + 'testMode' => true, + 'prefix' => 'PREFIX_' + ]); + + $database->query("SHOW TABLES LIKE "); + + $this->assertQuery( + <<queryString + ); + } + + /** + * @covers ::query() + * @covers ::isRaw() + * @covers ::buildRaw() + * @dataProvider typesProvider + */ + public function testPreparedStatementQuery($type) + { + $this->setType($type); + + $this->database->query( + "SELECT * FROM WHERE = :user_name AND = :age", + [ + ":user_name" => "John Smite", + ":age" => 20 + ] + ); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::query() + * @covers ::isRaw() + * @covers ::buildRaw() + */ + public function testQueryEscape() + { + $database = new Medoo([ + 'testMode' => true, + 'prefix' => 'PREFIX_' + ]); + + $database->query("SELECT * FROM WHERE = ''"); + + $this->assertQuery( + <<queryString + ); + } +} diff --git a/include/thirdparty/db/tests/QuoteTest.php b/include/thirdparty/db/tests/QuoteTest.php new file mode 100644 index 0000000..8d8133c --- /dev/null +++ b/include/thirdparty/db/tests/QuoteTest.php @@ -0,0 +1,123 @@ +setType($type); + + $quotedString = $this->database->quote("Co'mpl''ex \"st'\"ring"); + + $expected = [ + 'mysql' => << << << << <<assertEquals($expected[$type], $quotedString); + } + + /** + * @covers ::columnQuote() + */ + public function testColumnQuote() + { + $this->assertEquals('"ColumnName"', $this->database->columnQuote("ColumnName")); + $this->assertEquals('"Column"."name"', $this->database->columnQuote("Column.name")); + $this->assertEquals('"Column"."Name"', $this->database->columnQuote("Column.Name")); + + $this->assertEquals('"ネーム"', $this->database->columnQuote("ネーム")); + } + + public function columnNamesProvider(): array + { + return [ + ["9ColumnName"], + ["@ColumnName"], + [".ColumnName"], + ["ColumnName."], + ["ColumnName (alias)"] + ]; + } + + /** + * @covers ::columnQuote() + * @dataProvider columnNamesProvider + */ + public function testIncorrectColumnQuote($column) + { + $this->expectException(InvalidArgumentException::class); + + $this->database->columnQuote($column); + } + + /** + * @covers ::tableQuote() + */ + public function testTableQuote() + { + $this->assertEquals('"TableName"', $this->database->tableQuote("TableName")); + $this->assertEquals('"_table"', $this->database->tableQuote("_table")); + + $this->assertEquals('"アカウント"', $this->database->tableQuote("アカウント")); + } + + /** + * @covers ::tableQuote() + */ + public function testPrefixTableQuote() + { + $database = new Medoo([ + 'testMode' => true, + 'prefix' => 'PREFIX_' + ]); + + $this->assertEquals('"PREFIX_TableName"', $database->tableQuote("TableName")); + } + + public function tableNamesProvider(): array + { + return [ + ["9TableName"], + ["@TableName"], + [".TableName"], + ["TableName."], + ["Table.name"] + ]; + } + + /** + * @covers ::tableQuote() + * @dataProvider tableNamesProvider + */ + public function testIncorrectTableQuote($table) + { + $this->expectException(InvalidArgumentException::class); + + $this->database->tableQuote($table); + } +} diff --git a/include/thirdparty/db/tests/RandTest.php b/include/thirdparty/db/tests/RandTest.php new file mode 100644 index 0000000..2972317 --- /dev/null +++ b/include/thirdparty/db/tests/RandTest.php @@ -0,0 +1,153 @@ +setType($type); + + $this->database->rand("account", [ + "user_name" + ]); + + $this->assertQuery([ + 'default' => << << <<database->queryString); + } + + /** + * @covers ::rand() + * @dataProvider typesProvider + */ + public function testWhereRand($type) + { + $this->setType($type); + + $this->database->rand("account", [ + "user_name" + ], [ + "location" => "Tokyo" + ]); + + $this->assertQuery([ + 'default' => << << <<database->queryString); + } + + /** + * @covers ::rand() + * @dataProvider typesProvider + */ + public function testWhereWithJoinRand($type) + { + $this->setType($type); + + $this->database->rand("account", [ + "[>]album" => "user_id" + ], [ + "account.user_name" + ], [ + "album.location" => "Tokyo" + ]); + + $this->assertQuery([ + 'default' => << << <<database->queryString); + } + + /** + * @covers ::rand() + * @dataProvider typesProvider + */ + public function testWithJoinRand($type) + { + $this->setType($type); + + $this->database->rand("account", [ + "[>]album" => "user_id" + ], [ + "account.user_name" + ]); + + $this->assertQuery([ + 'default' => << << <<database->queryString); + } +} diff --git a/include/thirdparty/db/tests/RawTest.php b/include/thirdparty/db/tests/RawTest.php new file mode 100644 index 0000000..93fb3da --- /dev/null +++ b/include/thirdparty/db/tests/RawTest.php @@ -0,0 +1,57 @@ +setType($type); + + $this->database->select('account', [ + 'score' => Medoo::raw('SUM( + )') + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::raw() + * @covers ::isRaw() + * @covers ::buildRaw() + * @dataProvider typesProvider + */ + public function testRawWithSamePlaceholderName($type) + { + $this->setType($type); + + $this->database->select('account', [ + 'system' => Medoo::raw("COUNT( = 'window' OR = 'mac')") + ]); + + $this->assertQuery( + <<database->queryString + ); + } +} diff --git a/include/thirdparty/db/tests/ReplaceTest.php b/include/thirdparty/db/tests/ReplaceTest.php new file mode 100644 index 0000000..e645b85 --- /dev/null +++ b/include/thirdparty/db/tests/ReplaceTest.php @@ -0,0 +1,55 @@ +setType($type); + + $this->database->replace("account", [ + "type" => [ + "user" => "new_user", + "business" => "new_business" + ], + "column" => [ + "old_value" => "new_value" + ] + ], [ + "user_id[>]" => 1000 + ]); + + $this->assertQuery( + << 1000 + EOD, + $this->database->queryString + ); + } + + /** + * @covers ::replace() + */ + public function testReplaceEmptyColumns() + { + $this->expectException(InvalidArgumentException::class); + + $this->database->replace("account", [], [ + "user_id[>]" => 1000 + ]); + } +} diff --git a/include/thirdparty/db/tests/SelectTest.php b/include/thirdparty/db/tests/SelectTest.php new file mode 100644 index 0000000..d718c69 --- /dev/null +++ b/include/thirdparty/db/tests/SelectTest.php @@ -0,0 +1,715 @@ +setType($type); + + $this->database->select("account", "*"); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::selectContext() + * @dataProvider typesProvider + */ + public function testSelectTableWithAlias($type) + { + $this->setType($type); + + $this->database->select("account (user)", "name"); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::columnMap() + * @covers ::columnPush() + * @dataProvider typesProvider + */ + public function testSelectSingleColumn($type) + { + $this->setType($type); + + $this->database->select("account", "name"); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::columnMap() + * @covers ::columnPush() + * @dataProvider typesProvider + */ + public function testSelectColumns($type) + { + $this->setType($type); + + $this->database->select("account", ["name", "id"]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::columnMap() + * @covers ::columnPush() + * @dataProvider typesProvider + */ + public function testSelectColumnsWithAlias($type) + { + $this->setType($type); + + $this->database->select("account", ["name(nickname)", "id"]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::columnMap() + * @covers ::columnPush() + * @dataProvider typesProvider + */ + public function testSelectColumnsWithType($type) + { + $this->setType($type); + + $this->database->select("account", ["name[String]", "data [JSON]"]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::columnMap() + * @covers ::columnPush() + * @dataProvider typesProvider + */ + public function testSelectColumnsWithAliasAndType($type) + { + $this->setType($type); + + $this->database->select("account", ["name (nickname) [String]", "data [JSON]"]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::columnMap() + * @covers ::columnPush() + * @dataProvider typesProvider + */ + public function testSelectColumnsWithRaw($type) + { + $this->setType($type); + + $this->database->select("account", [ + "id [String]" => Medoo::raw("UUID()") + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::selectContext() + * @covers ::isJoin() + * @dataProvider typesProvider + */ + public function testSelectWithWhere($type) + { + $this->setType($type); + + $this->database->select("account", [ + "name", + "id" + ], [ + "ORDER" => "age" + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::selectContext() + * @covers ::isJoin() + * @covers ::buildJoin() + * @dataProvider typesProvider + */ + public function testSelectWithLeftJoin($type) + { + $this->setType($type); + + $this->database->select("account", [ + "[>]post" => "user_id" + ], [ + "account.name", + "post.title" + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::isJoin() + * @covers ::buildJoin() + * @dataProvider typesProvider + */ + public function testSelectWithRightJoin($type) + { + $this->setType($type); + + $this->database->select("account", [ + "[<]post" => "user_id" + ], [ + "account.name", + "post.title" + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::isJoin() + * @covers ::buildJoin() + * @dataProvider typesProvider + */ + public function testSelectWithFullJoin($type) + { + $this->setType($type); + + $this->database->select("account", [ + "[<>]post" => "user_id" + ], [ + "account.name", + "post.title" + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::isJoin() + * @covers ::buildJoin() + * @dataProvider typesProvider + */ + public function testSelectWithInnerJoin($type) + { + $this->setType($type); + + $this->database->select("account", [ + "[><]post" => "user_id" + ], [ + "account.name", + "post.title" + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::isJoin() + * @covers ::buildJoin() + * @dataProvider typesProvider + */ + public function testSelectWithSameKeysJoin($type) + { + $this->setType($type); + + $this->database->select("account", [ + "[>]photo" => ["user_id", "avatar_id"], + ], [ + "account.name", + "photo.link" + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::isJoin() + * @covers ::buildJoin() + * @dataProvider typesProvider + */ + public function testSelectWithKeyJoin($type) + { + $this->setType($type); + + $this->database->select("account", [ + "[>]post" => ["user_id" => "author_id"], + ], [ + "account.name", + "post.title" + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::isJoin() + * @covers ::buildJoin() + * @dataProvider typesProvider + */ + public function testSelectWithAliasJoin($type) + { + $this->setType($type); + + $this->database->select("account", [ + "[>]post (main_post)" => ["user_id" => "author_id"], + ], [ + "account.name", + "main_post.title" + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::isJoin() + * @covers ::buildJoin() + * @dataProvider typesProvider + */ + public function testSelectWithReferJoin($type) + { + $this->setType($type); + + $this->database->select("account", [ + "[>]post" => ["user_id" => "author_id"], + "[>]album" => ["post.author_id" => "user_id"], + ], [ + "account.name", + "post.title", + "album.link" + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::isJoin() + * @covers ::buildJoin() + * @dataProvider typesProvider + */ + public function testSelectWithMultipleConditionJoin($type) + { + $this->setType($type); + + $this->database->select("account", [ + "[>]album" => ["author_id" => "user_id"], + "[>]post" => [ + "user_id" => "author_id", + "album.user_id" => "owner_id" + ] + ], [ + "account.name", + "post.title", + "album.link" + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::isJoin() + * @covers ::buildJoin() + * @dataProvider typesProvider + */ + public function testSelectWithAdditionalConditionJoin($type) + { + $this->setType($type); + + $this->database->select("account", [ + "[>]post" => [ + "user_id" => "author_id", + "AND" => [ + "post.id[>]" => 10 + ] + ] + ], [ + "account.name", + "post.title" + ]); + + $this->assertQuery( + << 10 + EOD, + $this->database->queryString + ); + } + + /** + * @covers ::isJoin() + * @covers ::buildJoin() + * @dataProvider typesProvider + */ + public function testSelectRawJoin($type) + { + $this->setType($type); + + $this->database->select("account", [ + "[>]post" => Medoo::raw("ON = ") + ], [ + "account.name", + "post.title" + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::columnMap() + * @covers ::columnPush() + * @dataProvider typesProvider + */ + public function testSelectAllWithJoin($type) + { + $this->setType($type); + + $this->expectException(InvalidArgumentException::class); + + $this->database->select("account", [ + "[>]post" => "user_id" + ], [ + "account.*" + ]); + } + + /** + * @covers ::columnMap() + * @covers ::columnPush() + * @dataProvider typesProvider + */ + public function testSelectWithDataMapping($type) + { + $this->setType($type); + + $this->database->select("post", [ + "[>]account" => ["user_id"] + ], [ + "post.content", + + "userData" => [ + "account.user_id", + "account.email", + + "meta" => [ + "account.location", + "account.gender" + ] + ] + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::columnMap() + * @covers ::columnPush() + * @dataProvider typesProvider + */ + public function testSelectWithIndexMapping($type) + { + $this->setType($type); + + $this->database->select("account", [ + "user_id" => [ + "name (nickname)", + "location" + ] + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::columnMap() + * @covers ::columnPush() + * @dataProvider typesProvider + */ + public function testSelectWithDistinct($type) + { + $this->setType($type); + + $this->database->select("account", [ + "@location", + "nickname" + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::columnMap() + * @covers ::columnPush() + * @dataProvider typesProvider + */ + public function testSelectWithDistinctDiffOrder($type) + { + $this->setType($type); + + $this->database->select("account", [ + "location", + "@nickname" + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::columnMap() + * @covers ::columnPush() + * @dataProvider typesProvider + */ + public function testSelectWithUnicodeCharacter($type) + { + $this->setType($type); + + $this->database->select("considérer", [ + "name (名前)", + "положение (ロケーション)" + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::columnMap() + * @covers ::columnPush() + * @dataProvider typesProvider + */ + public function testSelectWithHyphenCharacter($type) + { + $this->setType($type); + + $this->database->select("account", [ + "nick-name" + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::columnMap() + * @covers ::columnPush() + * @dataProvider typesProvider + */ + public function testSelectWithSingleCharacter($type) + { + $this->setType($type); + + $this->database->select("a", [ + "[>]e" => ["f"] + ], [ + "b (c)" + ]); + + $this->assertQuery( + <<database->queryString + ); + } +} diff --git a/include/thirdparty/db/tests/UpdateTest.php b/include/thirdparty/db/tests/UpdateTest.php new file mode 100644 index 0000000..389cadf --- /dev/null +++ b/include/thirdparty/db/tests/UpdateTest.php @@ -0,0 +1,91 @@ +setType($type); + + $objectData = new Foo(); + + $this->database->update("account", [ + "type" => "user", + "money" => 23.2, + "age[+]" => 1, + "level[-]" => 5, + "score[*]" => 2, + "lang" => ["en", "fr"], + "lang [JSON]" => ["en", "fr"], + "is_locked" => true, + "uuid" => Medoo::raw("UUID()"), + "object" => $objectData + ], [ + "user_id[<]" => 1000 + ]); + + $this->assertQuery([ + 'default' => << <<database->queryString); + } + + public function testOracleLOBsUpdate() + { + $this->setType("oracle"); + + $fp = fopen('README.md', 'r'); + + $this->database->update("ACCOUNT", [ + "DATA" => $fp + ], [ + "ID" => 1 + ]); + + $this->assertQuery( + <<database->queryString + ); + } +} diff --git a/include/thirdparty/db/tests/WhereTest.php b/include/thirdparty/db/tests/WhereTest.php new file mode 100644 index 0000000..e058d7a --- /dev/null +++ b/include/thirdparty/db/tests/WhereTest.php @@ -0,0 +1,1177 @@ +setType($type); + + $this->database->select("account", "user_name", [ + "email" => "foo@bar.com", + "user_id" => 200, + "user_id[>]" => 200, + "user_id[>=]" => 200, + "user_id[!]" => 200, + "age[<>]" => [200, 500], + "age[><]" => [200, 500], + "income[>]" => Medoo::raw("COUNT()"), + "remote_id" => Medoo::raw("UUID()"), + "location" => null, + "is_selected" => true + ]); + + $this->assertQuery( + << 200 AND + "user_id" >= 200 AND + "user_id" != 200 AND + ("age" BETWEEN 200 AND 500) AND + ("age" NOT BETWEEN 200 AND 500) AND + "income" > COUNT("average") AND + "remote_id" = UUID() AND + "location" IS NULL AND + "is_selected" = 1 + EOD, + $this->database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + * @dataProvider typesProvider + */ + public function testBetweenDateTimeWhere($type) + { + $this->setType($type); + + $this->database->select("account", "user_name", [ + "birthday[<>]" => [date("Y-m-d", mktime(0, 0, 0, 1, 1, 2015)), date("Y-m-d", mktime(0, 0, 0, 1, 1, 2045))] + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + * @dataProvider typesProvider + */ + public function testNotBetweenDateTimeWhere($type) + { + $this->setType($type); + + $this->database->select("account", "user_name", [ + "birthday[><]" => [date("Y-m-d", mktime(0, 0, 0, 1, 1, 2015)), date("Y-m-d", mktime(0, 0, 0, 1, 1, 2045))] + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + * @dataProvider typesProvider + */ + public function testBetweenStringWhere($type) + { + $this->setType($type); + + $this->database->select("account", "user_name", [ + "location[<>]" => ['New York', 'Santo'] + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + * @dataProvider typesProvider + */ + public function testBetweenRawWhere($type) + { + $this->setType($type); + + $this->database->select("account", "user_name", [ + "birthday[<>]" => [ + Medoo::raw("to_date(:from, 'YYYY-MM-DD')", [":from" => '2015/05/15']), + Medoo::raw("to_date(:to, 'YYYY-MM-DD')", [":to" => '2025/05/15']) + ] + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + * @dataProvider typesProvider + */ + public function testGreaterDateTimeWhere($type) + { + $this->setType($type); + + $this->database->select("account", "user_name", [ + "birthday[>]" => date("Y-m-d", mktime(0, 0, 0, 1, 1, 2045)) + ]); + + $this->assertQuery( + << '2045-01-01' + EOD, + $this->database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + * @dataProvider typesProvider + */ + public function testArrayIntValuesWhere($type) + { + $this->setType($type); + + $this->database->select("account", "user_name", [ + "user_id" => [2, 123, 234, 54] + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + * @dataProvider typesProvider + */ + public function testArrayStringValuesWhere($type) + { + $this->setType($type); + + $this->database->select("account", "user_name", [ + "email" => ["foo@bar.com", "cat@dog.com", "admin@medoo.in"] + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + * @dataProvider typesProvider + */ + public function testRawArrayValuesWhere($type) + { + $this->setType($type); + + $this->database->select("account", "user_name", [ + 'id' => [ + Medoo::raw('LOWER("FOO")'), + Medoo::raw('LOWER("BAR")') + ] + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + * @dataProvider typesProvider + */ + public function testRawNotInArrayValuesWhere($type) + { + $this->setType($type); + + $this->database->select("account", "user_name", [ + 'id[!]' => [ + Medoo::raw('LOWER("FOO")'), + Medoo::raw('LOWER("BAR")') + ] + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + * @dataProvider typesProvider + */ + public function testNegativeWhere($type) + { + $this->setType($type); + + $this->database->select("account", "user_name", [ + "AND" => [ + "user_name[!]" => "foo", + "user_id[!]" => 1024, + "email[!]" => ["foo@bar.com", "admin@medoo.in"], + "city[!]" => null, + "promoted[!]" => true, + "location[!]" => Medoo::raw('LOWER("New York")') + ] + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + * @dataProvider typesProvider + */ + public function testBasicAndRelativityWhere($type) + { + $this->setType($type); + + $this->database->select("account", "user_name", [ + "AND" => [ + "user_id[>]" => 200, + "gender" => "female" + ] + ]); + + $this->assertQuery( + << 200 AND "gender" = 'female') + EOD, + $this->database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + * @dataProvider typesProvider + */ + public function testBasicSingleRelativityWhere($type) + { + $this->setType($type); + + $this->database->select("account", "user_name", [ + "user_id[>]" => 200, + "gender" => "female" + ]); + + $this->assertQuery( + << 200 AND "gender" = 'female' + EOD, + $this->database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + * @dataProvider typesProvider + */ + public function testBasicOrRelativityWhere($type) + { + $this->setType($type); + + $this->database->select("account", "user_name", [ + "OR" => [ + "user_id[>]" => 200, + "age[<>]" => [18, 25], + "gender" => "female" + ] + ]); + + $this->assertQuery( + << 200 OR + ("age" BETWEEN 18 AND 25) OR + "gender" = 'female') + EOD, + $this->database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + * @dataProvider typesProvider + */ + public function testCompoundRelativityWhere($type) + { + $this->setType($type); + + $this->database->select("account", "user_name", [ + "AND" => [ + "OR" => [ + "user_name" => "foo", + "email" => "foo@bar.com" + ], + "password" => "12345" + ] + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + * @dataProvider typesProvider + */ + public function testCompoundDuplicatedKeysWhere($type) + { + $this->setType($type); + + $this->database->select("account", "user_name", [ + "AND #comment" => [ + "OR #first comment" => [ + "user_name" => "foo", + "email" => "foo@bar.com" + ], + "OR #sencond comment" => [ + "user_name" => "bar", + "email" => "bar@foo.com" + ] + ] + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + * @dataProvider typesProvider + */ + public function testColumnsRelationshipWhere($type) + { + $this->setType($type); + + $this->database->select("post", [ + "[>]account" => "user_id", + ], [ + "post.content" + ], [ + "post.restrict[<]account.age", + "post.type[=]account.type" + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + * @dataProvider typesProvider + */ + public function testBasicLikeWhere($type) + { + $this->setType($type); + + $this->database->select("account", "user_name", [ + "city[~]" => "lon", + "name[~]" => "some-name" + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + * @dataProvider typesProvider + */ + public function testGroupedLikeWhere($type) + { + $this->setType($type); + + $this->database->select("account", "user_name", [ + "city[~]" => ["lon", "foo", "bar"] + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + * @dataProvider typesProvider + */ + public function testNegativeLikeWhere($type) + { + $this->setType($type); + + $this->database->select("account", "user_name", [ + "city[!~]" => "lon" + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + * @dataProvider typesProvider + */ + public function testNonEscapeLikeWhere($type) + { + $this->setType($type); + + $this->database->select("account", "user_name", [ + "city[~]" => "some_where", + "county[~]" => "[a-f]stan" + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + * @dataProvider typesProvider + */ + public function testEscapeLikeWhere($type) + { + $this->setType($type); + + $this->database->select("account", "user_name", [ + "city[~]" => "some\_where" + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + * @dataProvider typesProvider + */ + public function testCompoundLikeWhere($type) + { + $this->setType($type); + + $this->database->select("account", "user_name", [ + "content[~]" => ["AND" => ["lon", "on"]], + "city[~]" => ["OR" => ["lon", "on"]] + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + * @dataProvider typesProvider + */ + public function testWildcardLikeWhere($type) + { + $this->setType($type); + + $this->database->select("account", "user_name", [ + "city[~]" => "%stan", + "company[~]" => "Goo%", + "location[~]" => "Londo_", + "name[~]" => "[BCR]at", + "nickname[~]" => "[!BCR]at" + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + * @dataProvider typesProvider + */ + public function testMultipleLikeWhere($type) + { + $this->setType($type); + + $words = [ + "one", + "two", + "three", + "four", + "five", + "six", + "seven", + "eight", + "nine", + "ten", + "eleven", + "twelve" + ]; + + $this->database->select("account", ["title"], ["title[~]" => $words]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + * @dataProvider typesProvider + */ + public function testBasicOrderWhere($type) + { + $this->setType($type); + + $this->database->select("account", "user_name", [ + "ORDER" => "user_id" + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + * @dataProvider typesProvider + */ + public function testMultipleOrderWhere($type) + { + $this->setType($type); + + $this->database->select("account", "user_name", [ + "ORDER" => [ + // Order by column with sorting by customized order. + "user_id" => [43, 12, 57, 98, 144, 1], + + // Order by column. + "register_date", + + // Order by column with descending sorting. + "profile_id" => "DESC", + + // Order by column with ascending sorting. + "date" => "ASC" + ] + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + * @dataProvider typesProvider + */ + public function testOrderWithRawWhere($type) + { + $this->setType($type); + + $this->database->select("account", "user_name", [ + "ORDER" => Medoo::raw(", ") + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + */ + public function testFullTextSearchWhere() + { + $this->setType("mysql"); + + $this->database->select("account", "user_name", [ + "MATCH" => [ + "columns" => ["content", "title"], + "keyword" => "foo", + "mode" => "natural" + ] + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + * @dataProvider typesProvider + */ + public function testRegularExpressionWhere($type) + { + $this->setType($type); + + $this->database->select("account", "user_name", [ + 'user_name[REGEXP]' => '[a-z0-9]*' + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + * @dataProvider typesProvider + */ + public function testRawWhere($type) + { + $this->setType($type); + + $this->database->select("account", "user_name", [ + 'datetime' => Medoo::raw('NOW()') + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + * @dataProvider typesProvider + */ + public function testLimitWhere($type) + { + $this->setType($type); + + $this->database->select("account", "user_name", [ + 'LIMIT' => 100 + ]); + + $this->assertQuery([ + 'default' => << << <<database->queryString); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + * @dataProvider typesProvider + */ + public function testLimitOffsetWhere($type) + { + $this->setType($type); + + $this->database->select("account", "user_name", [ + 'LIMIT' => [20, 100] + ]); + + $this->assertQuery([ + 'default' => << << <<database->queryString); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + * @dataProvider typesProvider + */ + public function testGroupWhere($type) + { + $this->setType($type); + + $this->database->select("account", "user_name", [ + 'GROUP' => 'type', + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + * @dataProvider typesProvider + */ + public function testGroupWithArrayWhere($type) + { + $this->setType($type); + + $this->database->select("account", "user_name", [ + 'GROUP' => [ + 'type', + 'age', + 'gender' + ] + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + * @dataProvider typesProvider + */ + public function testGroupWithRawWhere($type) + { + $this->setType($type); + + $this->database->select("account", "user_name", [ + 'GROUP' => Medoo::raw(", ") + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + * @dataProvider typesProvider + */ + public function testHavingWhere($type) + { + $this->setType($type); + + $this->database->select("account", "user_name", [ + 'HAVING' => [ + 'user_id[>]' => 500 + ] + ]); + + $this->assertQuery( + << 500 + EOD, + $this->database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + * @dataProvider typesProvider + */ + public function testHavingWithRawWhere($type) + { + $this->setType($type); + + $this->database->select("account", "user_name", [ + 'HAVING' => Medoo::raw(' = LOWER("NEW YORK")') + ]); + + $this->assertQuery( + <<database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + * @dataProvider typesProvider + */ + public function testHavingWithAggregateRawWhere($type) + { + $this->setType($type); + + $this->database->select("account", [ + "total" => Medoo::raw('SUM()') + ], [ + 'HAVING' => Medoo::raw('SUM() > 1000') + ]); + + $this->assertQuery( + << 1000 + EOD, + $this->database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + * @dataProvider typesProvider + */ + public function testRawWhereClause($type) + { + $this->setType($type); + + $this->database->select( + "account", + "user_name", + Medoo::raw("WHERE => 10") + ); + + $this->assertQuery( + << 10 + EOD, + $this->database->queryString + ); + } + + /** + * @covers ::select() + * @covers ::dataImplode() + * @covers ::whereClause() + * @dataProvider typesProvider + */ + public function testRawWhereWithJoinClause($type) + { + $this->setType($type); + + $this->database->select( + "post", + [ + "[>]account" => "user_id", + ], + [ + "post.content" + ], + Medoo::raw("WHERE => 10") + ); + + $this->assertQuery( + << 10 + EOD, + $this->database->queryString + ); + } +}