diff --git a/install/migrations/update_9.4.3_to_9.4.5.php b/install/migrations/update_9.4.3_to_9.4.5.php index 3908cb09b1b..1e48dcc459c 100644 --- a/install/migrations/update_9.4.3_to_9.4.5.php +++ b/install/migrations/update_9.4.3_to_9.4.5.php @@ -50,7 +50,7 @@ function update943to945() $migration->setVersion('9.4.5'); /** Add OLA TTR begin date field to Tickets */ - $iterator = new DBmysqlIterator(null); + $iterator = new DBmysqlIterator($DB); $migration->addField( 'glpi_tickets', 'ola_ttr_begin_date', @@ -58,7 +58,7 @@ function update943to945() [ 'after' => 'olalevels_id_ttr', 'update' => $DB->quoteName('date'), // Assign ticket creation date by default - 'condition' => 'WHERE ' . $iterator->analyseCrit(['NOT' => ['olas_id_ttr' => '0']]), + 'condition' => sprintf('WHERE NOT(%s = %d)', $DB->quoteName('olas_id_ttr'), 0), ] ); /** /Add OLA TTR begin date field to Tickets */ diff --git a/src/AbstractQuery.php b/src/AbstractQuery.php index 1d4c9cb6d31..16b886d050d 100644 --- a/src/AbstractQuery.php +++ b/src/AbstractQuery.php @@ -39,6 +39,8 @@ abstract class AbstractQuery { protected ?string $alias = null; + /** @var array */ + protected array $values = []; /** * Create a query @@ -74,4 +76,21 @@ public function __toString() { return $this->getQuery(); } + + /** + * @return array + */ + public function getValues(): array + { + return $this->values; + } + + /** + * @param array $values + */ + public function setValues(array $values): static + { + $this->values = $values; + return $this; + } } diff --git a/src/Cartridge.php b/src/Cartridge.php index cdd6938763f..694cd663e6d 100644 --- a/src/Cartridge.php +++ b/src/Cartridge.php @@ -252,7 +252,7 @@ public function backToStock(array $input, $history = true) 'id' => $input['id'], ] ); - if ($result && ($DB->affectedRows() > 0)) { + if ($result && ($DB->getAffectedRows() > 0)) { $changesCartrige = [ 0, '', @@ -314,7 +314,7 @@ public function install($pID, $tID) 'date_use' => null, ] ); - if ($result && ($DB->affectedRows() > 0)) { + if ($result && ($DB->getAffectedRows() > 0)) { $changes = [ '0', '', @@ -373,7 +373,7 @@ public function uninstall($ID) if ( $result - && ($DB->affectedRows() > 0) + && ($DB->getAffectedRows() > 0) ) { $changes = [ '0', diff --git a/src/CartridgeItem.php b/src/CartridgeItem.php index ecdecf4ef13..79fc0f1e93a 100644 --- a/src/CartridgeItem.php +++ b/src/CartridgeItem.php @@ -202,7 +202,7 @@ public static function addCompatibleType($cartridgeitems_id, $printermodels_id) ]; $result = $DB->insert('glpi_cartridgeitems_printermodels', $params); - if ($result && ($DB->affectedRows() > 0)) { + if ($result && ($DB->getAffectedRows() > 0)) { return true; } } diff --git a/src/CommonDBTM.php b/src/CommonDBTM.php index 7c6734e13a3..9b2230fb02c 100644 --- a/src/CommonDBTM.php +++ b/src/CommonDBTM.php @@ -736,7 +736,7 @@ public function updateInDB($updates, $oldvalues = []) $tobeupdated, ['id' => $this->fields['id']] ); - $affected_rows = $DB->affectedRows(); + $affected_rows = $DB->getAffectedRows(); if (count($oldvalues) && $affected_rows > 0) { Log::constructHistory($this, $oldvalues, $this->fields); @@ -803,9 +803,8 @@ public function restoreInDB() $params['date_mod'] = $_SESSION["glpi_currenttime"]; } - if ($DB->update(static::getTable(), $params, ['id' => $this->fields['id']])) { - return true; - } + $DB->update(static::getTable(), $params, ['id' => $this->fields['id']]); + return true; } return false; } @@ -1903,7 +1902,15 @@ protected function manageLocks() ); foreach ($fields as $field) { try { - $DB->executeStatement($stmt, [$field]); + $DB->executeStatement( + $stmt, + [ + 'itemtype' => static::class, + 'items_id' => $this->fields['id'], + 'date_creation' => $_SESSION["glpi_currenttime"], + 'field' => $field, + ] + ); } catch (StatementException $e) { if ($e->getCode() != 1062) { throw new RuntimeException('Unable to add locked field!', code: $e->getCode(), previous: $e); diff --git a/src/CronTask.php b/src/CronTask.php index 8d9fa58485c..d6a9f8d559d 100644 --- a/src/CronTask.php +++ b/src/CronTask.php @@ -267,7 +267,7 @@ public function start(): bool ] ); - if ($DB->affectedRows() > 0) { + if ($DB->getAffectedRows() > 0) { $this->timer = microtime(true); $this->volume = 0; $log = new CronTaskLog(); @@ -353,7 +353,7 @@ public function end(?int $retcode, int $log_state = CronTaskLog::STATE_STOP): bo ] ); - if ($DB->affectedRows() > 0) { + if ($DB->getAffectedRows() > 0) { // No gettext for log but add gettext line to be parsed for pot generation // order is important for insertion in english in the database if ($log_state === CronTaskLog::STATE_ERROR) { diff --git a/src/CronTaskLog.php b/src/CronTaskLog.php index 1cab52f2c42..70ce7142881 100644 --- a/src/CronTaskLog.php +++ b/src/CronTaskLog.php @@ -79,7 +79,7 @@ public static function cleanOld($id, $days) ] ); - return $result ? $DB->affectedRows() : 0; + return $result ? $DB->getAffectedRows() : 0; } diff --git a/src/DBmysql.php b/src/DBmysql.php index 0175af1d949..c86a84bbd1b 100644 --- a/src/DBmysql.php +++ b/src/DBmysql.php @@ -33,10 +33,13 @@ * --------------------------------------------------------------------- */ +use Glpi\DBAL\Delete; +use Glpi\DBAL\Insert; use Glpi\DBAL\QueryExpression; use Glpi\DBAL\QueryParam; use Glpi\DBAL\QuerySubQuery; use Glpi\DBAL\QueryUnion; +use Glpi\DBAL\Update; use Glpi\Debug\Profile; use Glpi\Exception\Database\StatementException; use Glpi\System\Requirement\DbTimezones; @@ -221,6 +224,7 @@ class DBmysql * Indicates whether the data fetched from DB must be unsanitized. */ private bool $must_unsanitize_data = false; + private int $affected_rows = 0; /** * Constructor / Connect to the MySQL Database @@ -407,7 +411,8 @@ public function doQuery($query) $duration = (microtime(true) - $start_time) * 1000; $debug_data['time'] = $duration; - $debug_data['rows'] = $this->affectedRows(); + $this->affected_rows = (int) $this->dbh->affected_rows; + $debug_data['rows'] = $this->getAffectedRows(); // Trigger warning errors if any SQL warnings was produced by the query $sql_warnings = $this->fetchQueryWarnings(); // Ensure that we collect warning after affected rows @@ -473,7 +478,7 @@ public function prepare($query) } /** - * Give result from a sql result + * Give result from a SQL result * * @param mysqli_result $result MySQL result handler * @param int $i Row offset to give @@ -916,10 +921,23 @@ public function getField(string $table, string $field, $usecache = true): ?array * Get number of affected rows in previous MySQL operation * * @return int number of affected rows on success, and -1 if the last query failed. + * @deprecated 12 */ public function affectedRows() { - return (int) $this->dbh->affected_rows; + Toolbox::deprecated('This method does not work when statements are used!'); + return $this->getAffectedRows(); + } + + /** + * Get number of affected rows in previous MySQL operation + * Rely on a class variable since GLPI v12 and statements usage, as $this->dbh->affected_rows is not reliable when statements are used. + * + * @return int number of affected rows on success, and -1 if the last query failed. + */ + public function getAffectedRows(): int + { + return $this->affected_rows; } /** @@ -1241,7 +1259,7 @@ public static function quoteName($name) return $name; } - // do not quote alreay quoted names + // do not quote already quoted names if (preg_match('/^`[^`]+`$/', $name) === 1) { return $name; } @@ -1285,43 +1303,48 @@ public static function quoteValue($value) /** * Builds an insert statement * - * @since 9.3 - * * @param string $table Table name - * @param QuerySubQuery|array $params Array of field => value pairs or a QuerySubQuery for INSERT INTO ... SELECT + * @param QuerySubQuery|array $params Array of field => value pairs or a QuerySubQuery for INSERT INTO ... SELECT * @phpstan-param array|QuerySubQuery $params * - * @return string + * @return Insert */ - public function buildInsert($table, $params) + public function buildInsert(string $table, array|QuerySubQuery $params): Insert { $query = "INSERT INTO " . self::quoteName($table) . ' '; if ($params instanceof QuerySubQuery) { // INSERT INTO ... SELECT Query where the sub-query returns all columns needed for the insert $query .= $params->getQuery(); + $values = $params->getValues(); } else { $fields = []; + $parameters = []; $values = []; foreach ($params as $key => $value) { $fields[] = static::quoteName($key); if ($value instanceof QueryExpression) { - $values[] = $value->getValue(); + $parameters[] = $value->getValue(); + $values = array_merge($values, $value->getValues()); unset($params[$key]); } elseif ($value instanceof QueryParam) { - $values[] = $value->getValue(); + $parameters[] = $value->getValue(); } else { - $values[] = self::quoteValue($value); + $values[] = $value; + $parameters[] = '?'; } } $query .= "("; $query .= implode(', ', $fields); $query .= ") VALUES ("; - $query .= implode(", ", $values); + $query .= implode(', ', $parameters); $query .= ")"; } - return $query; + $insert = new Insert(); + return $insert + ->setSQL($query) + ->setValues($values); } /** @@ -1336,9 +1359,10 @@ public function buildInsert($table, $params) */ public function insert($table, $params) { - $this->doQuery( - $this->buildInsert($table, $params) - ); + $query = $this->buildInsert($table, $params); + $stmt = $this->prepare($query->getSQL()); + $this->executeStatement($stmt, $query->getValues()); + $this->affected_rows = (int) $stmt->affected_rows; return true; } @@ -1349,14 +1373,15 @@ public function insert($table, $params) * * @param string $table Table name * @param array $params Query parameters ([field name => field value) - * @param array $clauses Clauses to use. If not 'WHERE' key specified, will b the WHERE clause (@see DBmysqlIterator capabilities) + * @param array $clauses Clauses to use. If not 'WHERE' key specified, will be the WHERE clause (@see DBmysqlIterator capabilities) * @param array $joins JOINS criteria array * * @since 9.4.0 $joins parameter added - * @return string + * @return Update */ - public function buildUpdate($table, $params, $clauses, array $joins = []) + public function buildUpdate($table, $params, $clauses, array $joins = []): Update { + $values = []; //when no explicit "WHERE", we only have a WHERE clause. if (!isset($clauses['WHERE'])) { $clauses = ['WHERE' => $clauses]; @@ -1390,16 +1415,26 @@ public function buildUpdate($table, $params, $clauses, array $joins = []) $query .= " SET "; foreach ($params as $field => $value) { - if ($value instanceof QueryParam || $value instanceof QueryExpression) { - //no quote for query parameters nor expressions + if ($value instanceof QueryParam) { $query .= self::quoteName($field) . " = " . $value->getValue() . ", "; + } elseif ($value instanceof QueryExpression) { + $qvalues = $value->getValues(); + if (count($qvalues)) { + $query .= self::quoteName($field) . " = ?, "; + $values = array_merge($values, $qvalues); + } else { + $query .= self::quoteName($field) . " = " . $value->getValue() . ", "; + } } elseif ($value === null || $value === 'NULL' || $value === 'null') { - $query .= self::quoteName($field) . " = NULL, "; + $query .= self::quoteName($field) . " = ?, "; + $values[] = null; } elseif (is_bool($value)) { + $query .= self::quoteName($field) . " = ?, "; // transform boolean as int (prevent `false` to be transformed to empty string) - $query .= self::quoteName($field) . " = '" . (int) $value . "', "; + $values[] = (int) $value; } else { - $query .= self::quoteName($field) . " = " . self::quoteValue($value) . ", "; + $query .= self::quoteName($field) . " = ?, "; + $values[] = $value; } } $query = rtrim($query, ', '); @@ -1407,16 +1442,20 @@ public function buildUpdate($table, $params, $clauses, array $joins = []) $query .= " WHERE " . $this->iterator->analyseCrit($clauses['WHERE']); // ORDER BY - if (isset($clauses['ORDER']) && !empty($clauses['ORDER'])) { + if (!empty($clauses['ORDER'])) { $query .= $this->iterator->handleOrderClause($clauses['ORDER']); } - if (isset($clauses['LIMIT']) && !empty($clauses['LIMIT'])) { - $offset = (isset($clauses['START']) && !empty($clauses['START'])) ? $clauses['START'] : null; + if (!empty($clauses['LIMIT'])) { + $offset = (!empty($clauses['START'])) ? $clauses['START'] : null; $query .= $this->iterator->handleLimits($clauses['LIMIT'], $offset); } - return $query; + $values = array_merge($values, $this->iterator->getValues()); + $update = new Update(); + return $update + ->setSQL($query) + ->setValues($values); } /** @@ -1435,7 +1474,9 @@ public function buildUpdate($table, $params, $clauses, array $joins = []) public function update($table, $params, $where, array $joins = []) { $query = $this->buildUpdate($table, $params, $where, $joins); - $this->doQuery($query); + $stmt = $this->prepare($query->getSQL()); + $this->executeStatement($stmt, $query->getValues()); + $this->affected_rows = (int) $stmt->affected_rows; return true; } @@ -1454,7 +1495,8 @@ public function update($table, $params, $where, array $joins = []) public function updateOrInsert($table, $params, $where, $onlyone = true) { $query = $this->buildUpdateOrInsert($table, $params, $where, $onlyone); - $this->doQuery($query); + $stmt = $this->prepare($query->getSQL()); + $this->executeStatement($stmt, $query->getValues()); return true; } @@ -1463,10 +1505,8 @@ public function updateOrInsert($table, $params, $where, $onlyone = true) * @param array $params * @param array $where * @param bool $onlyone - * - * @return string */ - public function buildUpdateOrInsert($table, $params, $where, $onlyone = true): string + public function buildUpdateOrInsert($table, $params, $where, $onlyone = true): Update|Insert { $req = $this->request(array_merge(['FROM' => $table], $where)); $data = array_merge($where, $params); @@ -1489,7 +1529,7 @@ public function buildUpdateOrInsert($table, $params, $where, $onlyone = true): s * @param array $joins JOINS criteria array * * @since 9.4.0 $joins parameter added - * @return string + * @return Delete */ public function buildDelete($table, $where, array $joins = []) { @@ -1503,8 +1543,12 @@ public function buildDelete($table, $where, array $joins = []) $this->iterator = new DBmysqlIterator($this); $query .= $this->iterator->analyseJoins($joins); $query .= " WHERE " . $this->iterator->analyseCrit($where); + $values = $this->iterator->getValues(); - return $query; + $delete = new Delete(); + return $delete + ->setSQL($query) + ->setValues($values); } /** @@ -1522,7 +1566,9 @@ public function buildDelete($table, $where, array $joins = []) public function delete($table, $where, array $joins = []) { $query = $this->buildDelete($table, $where, $joins); - $this->doQuery($query); + $stmt = $this->prepare($query->getSQL()); + $this->executeStatement($stmt, $query->getValues()); + $this->affected_rows = (int) $stmt->affected_rows; return true; } @@ -1996,6 +2042,11 @@ private function bindStatementParams(mysqli_stmt $stmt, array $params, string|ar return; } $params = array_values($params); //no need for the keys + foreach ($params as &$param) { + if ($param === false) { + $param = 0; + } + } if ($types === null) { //no types specified, assume all strings @@ -2004,15 +2055,30 @@ private function bindStatementParams(mysqli_stmt $stmt, array $params, string|ar $types = implode('', $types); } - if (false === $stmt->bind_param($types, ...$params)) { + try { + if (false === $stmt->bind_param($types, ...$params)) { + throw new StatementException( + sprintf( + 'Error binding params in SQL query "%s": %s (%d).', + $this->current_query, + $stmt->error, + $stmt->errno + ), + $stmt->errno + ); + } + } catch (ArgumentCountError $e) { + $scount = substr_count($this->current_query, '?'); + $vcount = count($params); throw new StatementException( sprintf( - 'Error binding params in SQL query "%s": %s (%d).', - $this->current_query, - $stmt->error, - $stmt->errno + 'Number of placeholders (%d) in SQL statement does not match number of values (%d). SQL: %s', + $scount, + $vcount, + $this->current_query ), - $stmt->errno + $e->getCode(), + $e ); } } diff --git a/src/DBmysqlIterator.php b/src/DBmysqlIterator.php index ede350da0ca..f5d186a4e1d 100644 --- a/src/DBmysqlIterator.php +++ b/src/DBmysqlIterator.php @@ -36,8 +36,10 @@ use Glpi\DBAL\QueryExpression; use Glpi\DBAL\QueryParam; use Glpi\DBAL\QuerySubQuery; +use Glpi\Exception\Database\StatementException; use function Safe\preg_replace; +use function Safe\preg_replace_callback; use function Safe\preg_split; /** @@ -52,6 +54,8 @@ class DBmysqlIterator implements SeekableIterator, Countable private ?DBmysql $conn = null; // Current SQL query private ?string $sql = null; + /** @var array */ + private array $values = []; // Current result private mysqli_result|bool $res = false; @@ -117,8 +121,24 @@ public function __construct($dbconnexion) */ public function execute(array $criteria): self { + $this->values = []; //reset values $this->buildQuery($criteria); - $this->res = $this->conn ? $this->conn->doQuery($this->sql) : false; + $this->res = false; + if ($this->conn) { + if (($scount = substr_count($this->sql, '?')) != ($vcount = count($this->values))) { + throw new StatementException( + sprintf( + 'Number of placeholders (%d) in SQL statement does not match number of values (%d). SQL: %s', + $scount, + $vcount, + $this->sql + ) + ); + } + $stmt = $this->conn->prepare($this->sql); + $this->conn->executeStatement($stmt, $this->values); + $this->res = $stmt->get_result(); + } $this->count = $this->res instanceof mysqli_result ? $this->conn->numrows($this->res) : 0; $this->setPosition(0); return $this; @@ -271,8 +291,11 @@ public function buildQuery(array $criteria): void if ($table instanceof AbstractQuery) { $query = $table; $table = $query->getQuery(); + $this->values = array_merge($this->values, $query->getValues()); } elseif ($table instanceof QueryExpression) { - $table = $table->getValue(); + $query = $table; + $table = $query->getValue(); + $this->values = array_merge($this->values, $query->getValues()); } else { $table = DBmysql::quoteName($table); } @@ -414,8 +437,10 @@ private function handleFields(int|string $t, array|string|QueryExpression|Abstra } if ($f instanceof AbstractQuery) { + $this->values = array_merge($this->values, $f->getValues()); return $f->getQuery(); } elseif ($f instanceof QueryExpression) { + $this->values = array_merge($this->values, $f->getValues()); return $f->getValue(); } else { return DBmysql::quoteName($f); @@ -495,6 +520,14 @@ public function getSql() return preg_replace('/ +/', ' ', $this->sql); } + /** + * @return array + */ + public function getValues(): array + { + return $this->values; + } + /** * Destructor * @@ -507,6 +540,34 @@ public function __destruct() } } + /** + * Generate the SQL statement for a array of criteria + * + * @param array $crit Criteria + * @param string $bool Boolean operator (default AND) + * + * @return string + */ + public function rawAnalyseCrit($crit, $bool = "AND") + { + //to be used outside statements + $analyzed = $this->analyseCrit($crit, $bool); + //replace placeholder by values + $i = 0; + return preg_replace_callback( + '/\?/', + function () use (&$i) { + if (!array_key_exists($i, $this->values)) { + throw new LogicException("Not enough values provided for the placeholders in criteria."); + } + $value = $this->values[$i]; + $i++; + return $this->conn->quoteValue($value); + }, + $analyzed + ); + } + /** * Generate the SQL statement for a array of criteria * @@ -525,8 +586,10 @@ public function analyseCrit(array $crit, $bool = "AND") if (is_numeric($name)) { // no key and direct expression if ($value instanceof QueryExpression) { + $this->values = array_merge($this->values, $value->getValues()); $ret .= $value->getValue(); } elseif ($value instanceof QuerySubQuery) { + $this->values = array_merge($this->values, $value->getValues()); $ret .= $value->getQuery(); } else { // No Key case => recurse. @@ -611,12 +674,18 @@ private function analyseCriterion($value): string */ private function getCriterionValue($value): string { - return match (true) { - $value instanceof AbstractQuery => $value->getQuery(), - $value instanceof QueryExpression => $value->getValue(), - $value instanceof QueryParam => $value->getValue(), - default => $this->analyseCriterionValue($value) - }; + switch (true) { + case $value instanceof AbstractQuery: + $this->values = array_merge($this->values, $value->getValues()); + return $value->getQuery(); + case $value instanceof QueryExpression: + $this->values = array_merge($this->values, $value->getValues()); + return $value->getValue(); + case $value instanceof QueryParam: + return $value->getValue(); + default: + return $this->analyseCriterionValue($value); + } } /** @@ -628,13 +697,15 @@ private function analyseCriterionValue($value) { $crit_value = null; if (is_array($value)) { - $values = []; + $crit_value = '(' . str_repeat('?, ', count($value) - 1) . '?)'; foreach ($value as $v) { - $values[] = DBmysql::quoteValue($v); + if (!($v instanceof QueryParam)) { + $this->values[] = $v; + } } - $crit_value = '(' . implode(', ', $values) . ')'; } else { - $crit_value = DBmysql::quoteValue($value); + $crit_value = '?'; + $this->values[] = $value; } return $crit_value; } @@ -669,6 +740,7 @@ public function analyseJoins(array $joinarray) // QueryExpression support, can be removed once Search::getDefaultJoin no longer returns raw SQL if ($jointablecrit instanceof QueryExpression) { $query .= $jointablecrit->getValue(); + $this->values = array_merge($this->values, $jointablecrit->getValues()); continue; } @@ -709,12 +781,18 @@ private function analyseFkey($values): string $left_value = $f1 instanceof QuerySubQuery || $f1 instanceof QueryExpression ? (string) $f1 : (is_numeric($t1) ? DBmysql::quoteName($f1) : DBmysql::quoteName($t1) . '.' . DBmysql::quoteName($f1)); + if ($f1 instanceof QuerySubQuery || $f1 instanceof QueryExpression) { + $this->values = array_merge($this->values, $f1->getValues()); + } $t2 = $keys[1]; $f2 = $values[$t2]; $right_value = $f2 instanceof QuerySubQuery || $f2 instanceof QueryExpression ? (string) $f2 : (is_numeric($t2) ? DBmysql::quoteName($f2) : DBmysql::quoteName($t2) . '.' . DBmysql::quoteName($f2)); + if ($f2 instanceof QuerySubQuery || $f2 instanceof QueryExpression) { + $this->values = array_merge($this->values, $f2->getValues()); + } return $left_value . ' = ' . $right_value; } elseif (count($values) == 3) { diff --git a/src/Glpi/DBAL/Delete.php b/src/Glpi/DBAL/Delete.php new file mode 100644 index 00000000000..f7a2607b32f --- /dev/null +++ b/src/Glpi/DBAL/Delete.php @@ -0,0 +1,37 @@ +. + * + * --------------------------------------------------------------------- + */ + +namespace Glpi\DBAL; + +class Delete extends Prepared {} diff --git a/src/Glpi/DBAL/Insert.php b/src/Glpi/DBAL/Insert.php new file mode 100644 index 00000000000..71f294a897e --- /dev/null +++ b/src/Glpi/DBAL/Insert.php @@ -0,0 +1,37 @@ +. + * + * --------------------------------------------------------------------- + */ + +namespace Glpi\DBAL; + +class Insert extends Prepared {} diff --git a/src/Glpi/DBAL/JsonFieldInterface.php b/src/Glpi/DBAL/JsonFieldInterface.php index a91141afb8f..a36b7763166 100644 --- a/src/Glpi/DBAL/JsonFieldInterface.php +++ b/src/Glpi/DBAL/JsonFieldInterface.php @@ -8,7 +8,6 @@ * http://glpi-project.org * * @copyright 2015-2026 Teclib' and contributors. - * @copyright 2003-2014 by the INDEPNET Development Team. * @licence https://www.gnu.org/licenses/gpl-3.0.html * * --------------------------------------------------------------------- diff --git a/src/Glpi/DBAL/Prepared.php b/src/Glpi/DBAL/Prepared.php new file mode 100644 index 00000000000..8879edcfc2e --- /dev/null +++ b/src/Glpi/DBAL/Prepared.php @@ -0,0 +1,75 @@ +. + * + * --------------------------------------------------------------------- + */ + +namespace Glpi\DBAL; + +abstract class Prepared +{ + protected string $sql; + /** @var array */ + protected array $values; + + public function setSQL(string $sql): static + { + $this->sql = $sql; + return $this; + } + + public function getSQL(): string + { + return $this->sql; + } + + /** + * @param array $values + */ + public function setValues(array $values): static + { + $this->values = $values; + return $this; + } + + /** + * @return array + */ + public function getValues(): array + { + return $this->values; + } + + public function __toString() + { + return $this->getSQL(); + } +} diff --git a/src/Glpi/DBAL/QueryExpression.php b/src/Glpi/DBAL/QueryExpression.php index 06fedfbba96..7b96030ccdc 100644 --- a/src/Glpi/DBAL/QueryExpression.php +++ b/src/Glpi/DBAL/QueryExpression.php @@ -8,7 +8,6 @@ * http://glpi-project.org * * @copyright 2015-2026 Teclib' and contributors. - * @copyright 2003-2014 by the INDEPNET Development Team. * @licence https://www.gnu.org/licenses/gpl-3.0.html * * --------------------------------------------------------------------- @@ -45,6 +44,9 @@ class QueryExpression private string $expression; private ?string $alias; + /** @var array */ + private array $values = []; + /** * Create a query expression @@ -82,4 +84,21 @@ public function __toString() { return $this->getValue(); } + + /** + * @return array + */ + public function getValues(): array + { + return $this->values; + } + + /** + * @param array $values + */ + public function setValues(array $values): static + { + $this->values = $values; + return $this; + } } diff --git a/src/Glpi/DBAL/QueryFunction.php b/src/Glpi/DBAL/QueryFunction.php index f14ba59fd3d..28e64e9d7de 100644 --- a/src/Glpi/DBAL/QueryFunction.php +++ b/src/Glpi/DBAL/QueryFunction.php @@ -8,7 +8,6 @@ * http://glpi-project.org * * @copyright 2015-2026 Teclib' and contributors. - * @copyright 2003-2014 by the INDEPNET Development Team. * @licence https://www.gnu.org/licenses/gpl-3.0.html * * --------------------------------------------------------------------- @@ -72,14 +71,16 @@ class QueryFunction * The alias should not be quoted. It will be done in the returned QueryExpression when its value is evaluated. * @param string $func_name SQL function name * @param array $params Array of quoted identifiers or QueryExpressions + * @param array $values Array of statement values * @param string|null $alias Unquoted alias * @return QueryExpression */ - private static function getExpression(string $func_name, array $params, ?string $alias = null): QueryExpression + private static function getExpression(string $func_name, array $params, array $values, ?string $alias = null): QueryExpression { global $DB; $params = array_map(static fn($p) => $p instanceof QueryExpression || $p === null ? $p : $DB::quoteName($p), $params); - return new QueryExpression($func_name . '(' . implode(', ', $params) . ')', $alias); + $qexpr = new QueryExpression($func_name . '(' . implode(', ', $params) . ')', $alias); + return $qexpr->setValues($values); } /** @@ -104,7 +105,8 @@ public static function __callStatic(string $name, array $arguments) default => $name, }; $func_name = strtoupper($func_name); - return self::getExpression($func_name, $params, $args[1] ?? null); + //FIXME: no idea how to handle possible statement values, and even if there are some... + return self::getExpression($func_name, $params, [], $args[1] ?? null); } /** @@ -151,10 +153,21 @@ public static function dateSub(string|QueryExpression $date, int|string|QueryExp */ public static function if(string|QueryExpression|array $condition, string|QueryExpression $true_expression, string|QueryExpression $false_expression, ?string $alias = null): QueryExpression { + $values = []; if (is_array($condition)) { - $condition = new QueryExpression((new DBmysqlIterator(null))->analyseCrit($condition)); + $iterator = new DBmysqlIterator(null); + $condition = new QueryExpression($iterator->analyseCrit($condition)); + $values = $iterator->getValues(); + } elseif ($condition instanceof QueryExpression) { + $values = array_merge($values, $condition->getValues()); } - return self::getExpression('IF', [$condition, $true_expression, $false_expression], $alias); + if ($true_expression instanceof QueryExpression) { + $values = array_merge($values, $true_expression->getValues()); + } + if ($false_expression instanceof QueryExpression) { + $values = array_merge($values, $false_expression->getValues()); + } + return self::getExpression('IF', [$condition, $true_expression, $false_expression], $values, $alias); } /** @@ -166,7 +179,14 @@ public static function if(string|QueryExpression|array $condition, string|QueryE */ public static function ifnull(string|QueryExpression $expression, string|QueryExpression $value, ?string $alias = null): QueryExpression { - return self::getExpression('IFNULL', [$expression, $value], $alias); + $values = []; + if ($expression instanceof QueryExpression) { + $values = array_merge($values, $expression->getValues()); + } + if ($value instanceof QueryExpression) { + $values = array_merge($values, $value->getValues()); + } + return self::getExpression('IFNULL', [$expression, $value], $values, $alias); } /** @@ -297,7 +317,17 @@ public static function curdate(?string $alias = null): QueryExpression */ public static function replace(string|QueryExpression $expression, string|QueryExpression $search, string|QueryExpression $replace, ?string $alias = null): QueryExpression { - return self::getExpression('REPLACE', [$expression, $search, $replace], $alias); + $values = []; + if ($expression instanceof QueryExpression) { + $values = array_merge($values, $expression->getValues()); + } + if ($search instanceof QueryExpression) { + $values = array_merge($values, $search->getValues()); + } + if ($replace instanceof QueryExpression) { + $values = array_merge($values, $replace->getValues()); + } + return self::getExpression('REPLACE', [$expression, $search, $replace], $values, $alias); } /** @@ -310,10 +340,17 @@ public static function replace(string|QueryExpression $expression, string|QueryE public static function fromUnixtime(string|QueryExpression $expression, string|QueryExpression|null $format = null, ?string $alias = null): QueryExpression { $params = [$expression]; + $values = []; if ($format !== null) { $params[] = $format; } - return self::getExpression('FROM_UNIXTIME', $params, $alias); + if ($expression instanceof QueryExpression) { + $values = array_merge($values, $expression->getValues()); + } + if ($format instanceof QueryExpression) { + $values = array_merge($values, $format->getValues()); + } + return self::getExpression('FROM_UNIXTIME', $params, $values, $alias); } /** @@ -326,8 +363,12 @@ public static function fromUnixtime(string|QueryExpression $expression, string|Q public static function dateFormat(string|QueryExpression $expression, string $format, ?string $alias = null): QueryExpression { global $DB; + $values = []; + if ($expression instanceof QueryExpression) { + $values = array_merge($values, $expression->getValues()); + } $format = new QueryExpression($DB::quoteValue($format)); - return self::getExpression('DATE_FORMAT', [$expression, $format], $alias); + return self::getExpression('DATE_FORMAT', [$expression, $format], $values, $alias); } /** @@ -341,9 +382,15 @@ public static function dateFormat(string|QueryExpression $expression, string $fo public static function lpad(string|QueryExpression $expression, int $length, string $pad_string, ?string $alias = null): QueryExpression { global $DB; + $values = []; + if ($expression instanceof QueryExpression) { + $values = array_merge($values, $expression->getValues()); + } $length = new QueryExpression((string) $length); + $values = array_merge($values, $length->getValues()); $pad_string = new QueryExpression($DB::quoteValue($pad_string)); - return self::getExpression('LPAD', [$expression, $length, $pad_string], $alias); + $values = array_merge($values, $pad_string->getValues()); + return self::getExpression('LPAD', [$expression, $length, $pad_string], $values, $alias); } /** @@ -356,9 +403,16 @@ public static function lpad(string|QueryExpression $expression, int $length, str */ public static function substring(string|QueryExpression $expression, int $start, int $length, ?string $alias = null): QueryExpression { + $start_expr = new QueryExpression((string) $start); + $length_expr = new QueryExpression((string) $length); + $values = []; + if ($expression instanceof QueryExpression) { + $values = array_merge($values, $expression->getValues()); + } + $values = array_merge($values, $start_expr->getValues(), $length_expr->getValues()); return self::getExpression('SUBSTRING', [ - $expression, new QueryExpression((string) $start), new QueryExpression((string) $length), - ], $alias); + $expression, $start_expr, $length_expr, + ], $values, $alias); } /** @@ -370,8 +424,13 @@ public static function substring(string|QueryExpression $expression, int $start, */ public static function round(string|QueryExpression $expression, int $precision = 0, ?string $alias = null): QueryExpression { + $values = []; + if ($expression instanceof QueryExpression) { + $values = array_merge($values, $expression->getValues()); + } $precision = new QueryExpression((string) $precision); - return self::getExpression('ROUND', [$expression, $precision], $alias); + $values = array_merge($values, $precision->getValues()); + return self::getExpression('ROUND', [$expression, $precision], $values, $alias); } /** @@ -383,7 +442,14 @@ public static function round(string|QueryExpression $expression, int $precision */ public static function nullif(string|QueryExpression $expression, string|QueryExpression $value, ?string $alias = null): QueryExpression { - return self::getExpression('NULLIF', [$expression, $value], $alias); + $values = []; + if ($expression instanceof QueryExpression) { + $values = array_merge($values, $expression->getValues()); + } + if ($value instanceof QueryExpression) { + $values = array_merge($values, $value->getValues()); + } + return self::getExpression('NULLIF', [$expression, $value], $values, $alias); } /** @@ -396,7 +462,19 @@ public static function nullif(string|QueryExpression $expression, string|QueryEx */ public static function timestampdiff(string $unit, string|QueryExpression $expression1, string|QueryExpression $expression2, ?string $alias = null): QueryExpression { - return self::getExpression('TIMESTAMPDIFF', [new QueryExpression($unit), $expression1, $expression2], $alias); + $values = []; + if ($expression1 instanceof QueryExpression) { + $values = array_merge($values, $expression1->getValues()); + } + $unit_expr = new QueryExpression($unit); + $values = array_merge($values, $unit_expr->getValues()); + if ($expression1 instanceof QueryExpression) { + $values = array_merge($values, $expression1->getValues()); + } + if ($expression2 instanceof QueryExpression) { + $values = array_merge($values, $expression2->getValues()); + } + return self::getExpression('TIMESTAMPDIFF', [$unit_expr, $expression1, $expression2], $values, $alias); } /** @@ -408,7 +486,14 @@ public static function timestampdiff(string $unit, string|QueryExpression $expre */ public static function datediff(string|QueryExpression $expression1, string|QueryExpression $expression2, ?string $alias = null): QueryExpression { - return self::getExpression('DATEDIFF', [$expression1, $expression2], $alias); + $values = []; + if ($expression1 instanceof QueryExpression) { + $values = array_merge($values, $expression1->getValues()); + } + if ($expression2 instanceof QueryExpression) { + $values = array_merge($values, $expression2->getValues()); + } + return self::getExpression('DATEDIFF', [$expression1, $expression2], $values, $alias); } /** @@ -420,7 +505,14 @@ public static function datediff(string|QueryExpression $expression1, string|Quer */ public static function timediff(string|QueryExpression $expression1, string|QueryExpression $expression2, ?string $alias = null): QueryExpression { - return self::getExpression('TIMEDIFF', [$expression1, $expression2], $alias); + $values = []; + if ($expression1 instanceof QueryExpression) { + $values = array_merge($values, $expression1->getValues()); + } + if ($expression2 instanceof QueryExpression) { + $values = array_merge($values, $expression2->getValues()); + } + return self::getExpression('TIMEDIFF', [$expression1, $expression2], $values, $alias); } /** @@ -432,10 +524,14 @@ public static function timediff(string|QueryExpression $expression1, string|Quer public static function unixTimestamp(string|QueryExpression|null $expression = null, ?string $alias = null): QueryExpression { $params = []; + $values = []; if ($expression !== null) { $params = [$expression]; + if ($expression instanceof QueryExpression) { + $values = array_merge($values, $expression->getValues()); + } } - return self::getExpression('UNIX_TIMESTAMP', $params, $alias); + return self::getExpression('UNIX_TIMESTAMP', $params, $values, $alias); } /** @@ -449,7 +545,11 @@ public static function locate(string|QueryExpression $substring, string|QueryExp { global $DB; $substring = is_string($substring) ? new QueryExpression($DB::quoteValue($substring)) : $substring; - return self::getExpression('LOCATE', [$substring, $expression], $alias); + $values = $substring->getValues(); + if ($expression instanceof QueryExpression) { + $values = array_merge($values, $expression->getValues()); + } + return self::getExpression('LOCATE', [$substring, $expression], $values, $alias); } /** @@ -498,6 +598,7 @@ public static function jsonContains(string|QueryExpression $target, string|Query { global $DB; + $values = []; if (is_string($target)) { $target = new QueryExpression($DB::quoteName($target)); } @@ -507,6 +608,7 @@ public static function jsonContains(string|QueryExpression $target, string|Query } $path = new QueryExpression($DB::quoteValue($path)); + $values = array_merge($values, $candidate->getValues()); return self::getExpression( 'JSON_CONTAINS', @@ -515,6 +617,7 @@ public static function jsonContains(string|QueryExpression $target, string|Query $DB->getVersionAndServer()['server'] === 'MariaDB' ? $candidate : QueryFunction::cast($candidate, 'JSON'), $path, ], + $values, $alias ); } diff --git a/src/Glpi/DBAL/QueryParam.php b/src/Glpi/DBAL/QueryParam.php index 35c9538e860..3952447788a 100644 --- a/src/Glpi/DBAL/QueryParam.php +++ b/src/Glpi/DBAL/QueryParam.php @@ -8,7 +8,6 @@ * http://glpi-project.org * * @copyright 2015-2026 Teclib' and contributors. - * @copyright 2003-2014 by the INDEPNET Development Team. * @licence https://www.gnu.org/licenses/gpl-3.0.html * * --------------------------------------------------------------------- diff --git a/src/Glpi/DBAL/QuerySubQuery.php b/src/Glpi/DBAL/QuerySubQuery.php index 02d30f60dd8..9cc00837495 100644 --- a/src/Glpi/DBAL/QuerySubQuery.php +++ b/src/Glpi/DBAL/QuerySubQuery.php @@ -8,7 +8,6 @@ * http://glpi-project.org * * @copyright 2015-2026 Teclib' and contributors. - * @copyright 2003-2014 by the INDEPNET Development Team. * @licence https://www.gnu.org/licenses/gpl-3.0.html * * --------------------------------------------------------------------- @@ -83,6 +82,14 @@ public function getQuery() return $sql; } + /** + * @return array + */ + public function getValues(): array + { + return $this->dbiterator->getValues(); + } + public function __toString() { return $this->getQuery(); diff --git a/src/Glpi/DBAL/QueryUnion.php b/src/Glpi/DBAL/QueryUnion.php index 3a0fd5ec593..203b8f64615 100644 --- a/src/Glpi/DBAL/QueryUnion.php +++ b/src/Glpi/DBAL/QueryUnion.php @@ -8,7 +8,6 @@ * http://glpi-project.org * * @copyright 2015-2026 Teclib' and contributors. - * @copyright 2003-2014 by the INDEPNET Development Team. * @licence https://www.gnu.org/licenses/gpl-3.0.html * * --------------------------------------------------------------------- @@ -124,4 +123,13 @@ public function getQuery() return $query; } + + public function getValues(): array + { + $values = []; + foreach ($this->queries as $query) { + $values = array_merge($values, $query->getValues()); + } + return $values; + } } diff --git a/src/Glpi/DBAL/Update.php b/src/Glpi/DBAL/Update.php new file mode 100644 index 00000000000..de52f76b0c4 --- /dev/null +++ b/src/Glpi/DBAL/Update.php @@ -0,0 +1,37 @@ +. + * + * --------------------------------------------------------------------- + */ + +namespace Glpi\DBAL; + +class Update extends Prepared {} diff --git a/src/Glpi/Dashboard/Provider.php b/src/Glpi/Dashboard/Provider.php index 8f9379d7586..7f27a6ca9dc 100644 --- a/src/Glpi/Dashboard/Provider.php +++ b/src/Glpi/Dashboard/Provider.php @@ -685,12 +685,24 @@ public static function nbTicketsByAgreementStatusAndTechnicianGroup( $ownExceeded = Ticket::generateSLAOLAComputation('time_to_own', $table); $resolveExceeded = Ticket::generateSLAOLAComputation('time_to_resolve', $table); $slaState = "IF ($ownExceeded AND $resolveExceeded, 3, IF ($resolveExceeded, 2, IF ($ownExceeded, 1, 0)))"; + $qexpr = new QueryExpression( + "IF ($ownExceeded AND $resolveExceeded, 3, IF ($resolveExceeded, 2, IF ($ownExceeded, 1, 0)))", + 'sla_state' + ); + $qexpr->setValues( + array_merge( + $ownExceeded->getValues(), + $resolveExceeded->getValues(), + $resolveExceeded->getValues(), + $ownExceeded->getValues(), + ) + ); $query_criteria = [ 'COUNT' => 'cpt', 'SELECT' => [ "$groupTable.name", - new QueryExpression("$slaState as `sla_state`"), + $qexpr, ], 'FROM' => $table, 'INNER JOIN' => [ diff --git a/src/Glpi/Event.php b/src/Glpi/Event.php index 22e57d40fb7..41ea9d8a7a5 100644 --- a/src/Glpi/Event.php +++ b/src/Glpi/Event.php @@ -176,7 +176,7 @@ public static function cleanOld($day) new QueryExpression("UNIX_TIMESTAMP(date) < UNIX_TIMESTAMP()-$secs"), ] ); - return $DB->affectedRows(); + return $DB->getAffectedRows(); } /** diff --git a/src/Glpi/Inventory/Asset/InventoryNetworkPort.php b/src/Glpi/Inventory/Asset/InventoryNetworkPort.php index 46b110d3d00..caac94276d0 100644 --- a/src/Glpi/Inventory/Asset/InventoryNetworkPort.php +++ b/src/Glpi/Inventory/Asset/InventoryNetworkPort.php @@ -457,7 +457,7 @@ private function handleUpdates() $netname_stmt = $DB->prepare($query); } - $DB->executeStatement($netname_stmt, [$keydb]); + $DB->executeStatement($netname_stmt, [NetworkPort::class, $keydb]); $results = $netname_stmt->get_result(); if ($results->num_rows) { diff --git a/src/Log.php b/src/Log.php index 9f1b95b62c6..27993565fa6 100644 --- a/src/Log.php +++ b/src/Log.php @@ -314,7 +314,7 @@ public static function history($items_id, $itemtype, $changes, $itemtype_link = $result = $DB->insert(self::getTable(), $params); - if ($result && $DB->affectedRows() > 0) { + if ($result && $DB->getAffectedRows() > 0) { return $_SESSION['glpi_maxhistory'] = $DB->insertId(); } return false; diff --git a/src/Migration.php b/src/Migration.php index ede575f51ba..7101c77c7d9 100644 --- a/src/Migration.php +++ b/src/Migration.php @@ -33,8 +33,10 @@ * --------------------------------------------------------------------- */ +use Glpi\DBAL\Prepared; use Glpi\DBAL\QueryExpression; use Glpi\DBAL\QueryFunction; +use Glpi\Exception\Database\StatementException; use Glpi\Message\MessageType; use Glpi\Progress\AbstractProgressIndicator; @@ -763,7 +765,12 @@ public function migrationOneTable($table) public function executeMigration() { foreach ($this->queries[self::PRE_QUERY] as $query) { - $this->db->doQuery($query['query']); + if ($query['query'] instanceof Prepared) { + $stmt = $this->db->prepare($query['query']->getSQL()); + $this->db->executeStatement($stmt, $query['query']->getValues()); + } else { + $this->db->doQuery($query['query']); + } } $this->queries[self::PRE_QUERY] = []; @@ -777,7 +784,12 @@ public function executeMigration() } foreach ($this->queries[self::POST_QUERY] as $query) { - $this->db->doQuery($query['query']); + if ($query['query'] instanceof Prepared) { + $stmt = $this->db->prepare($query['query']->getSQL()); + $this->db->executeStatement($stmt, $query['query']->getValues()); + } else { + $this->db->doQuery($query['query']); + } } $this->queries[self::POST_QUERY] = []; @@ -1090,14 +1102,20 @@ private function storeConfig() } if (count($config)) { foreach ($config as $name => $value) { - $this->db->insert( - 'glpi_configs', - [ - 'context' => $context, - 'name' => $name, - 'value' => $value, - ] - ); + try { + $this->db->insert( + 'glpi_configs', + [ + 'context' => $context, + 'name' => $name, + 'value' => $value, + ] + ); + } catch (StatementException $e) { + if (!str_contains($e->getMessage(), 'Duplicate entry')) { + throw $e; + } + } } $this->addDebugMessage(sprintf( __('Configuration values added for %1$s (%2$s).'), diff --git a/src/Profile.php b/src/Profile.php index 82ae63903f7..679accac700 100644 --- a/src/Profile.php +++ b/src/Profile.php @@ -717,7 +717,8 @@ public static function getUnderActiveProfileRestrictCriteria() 'OR' => $right_subqueries, ], ]); - $criteria[] = new QueryExpression(count($right_subqueries) . " = " . $sub_query->getQuery()); + $qexpr = new QueryExpression(count($right_subqueries) . " = " . $sub_query->getQuery()); + $criteria[] = $qexpr->setValues($sub_query->getValues()); if (Session::getCurrentInterface() === 'central') { return [ diff --git a/src/ProfileRight.php b/src/ProfileRight.php index b6c08ae6868..86e771d5c6c 100644 --- a/src/ProfileRight.php +++ b/src/ProfileRight.php @@ -225,12 +225,15 @@ public static function fillProfileRights($profiles_id) ]); $expr = 'NOT EXISTS ' . $subq->getQuery(); + $qexpr = new QueryExpression($expr); + $qexpr->setValues($subq->getValues()); + $iterator = $DB->request([ 'SELECT' => 'POSSIBLE.name AS NAME', 'DISTINCT' => true, 'FROM' => 'glpi_profilerights AS POSSIBLE', 'WHERE' => [ - new QueryExpression($expr), + $qexpr, ], ]); diff --git a/src/QueuedNotification.php b/src/QueuedNotification.php index 6d5ddfeaafe..7560d15efb1 100644 --- a/src/QueuedNotification.php +++ b/src/QueuedNotification.php @@ -688,7 +688,7 @@ public static function cronQueuedNotificationClean(?CronTask $task = null) new QueryExpression(QueryFunction::unixTimestamp('send_time') . ' < ' . $DB::quoteValue($send_time)), ] ); - $vol = $DB->affectedRows(); + $vol = $DB->getAffectedRows(); } $task->setVolume($vol); @@ -726,7 +726,7 @@ public static function cronQueuedNotificationCleanStaleAjax(?CronTask $task = nu ), ] ); - $vol = $DB->affectedRows(); + $vol = $DB->getAffectedRows(); } $task->setVolume($vol); diff --git a/src/QueuedWebhook.php b/src/QueuedWebhook.php index c996b5515a1..1153dc95083 100644 --- a/src/QueuedWebhook.php +++ b/src/QueuedWebhook.php @@ -675,7 +675,7 @@ public static function cronQueuedWebhookClean(?CronTask $task = null) ], ] ); - $vol = $DB->affectedRows(); + $vol = $DB->getAffectedRows(); } $task->setVolume($vol); diff --git a/tests/functional/CommonDBTMTest.php b/tests/functional/CommonDBTMTest.php index 0bfab64586a..c8ea94cfa3f 100644 --- a/tests/functional/CommonDBTMTest.php +++ b/tests/functional/CommonDBTMTest.php @@ -151,7 +151,7 @@ public function testGetFromDBByRequest() $instance = new Computer(); $this->expectExceptionMessage( - '`Computer::getFromDBByRequest()` expects to get one result, 2 found in query "SELECT `glpi_computers`.* FROM `glpi_computers` WHERE `contact` = \'johndoe\'".' + '`Computer::getFromDBByRequest()` expects to get one result, 2 found in query "SELECT `glpi_computers`.* FROM `glpi_computers` WHERE `contact` = ?".' ); $instance->getFromDbByRequest([ 'WHERE' => ['contact' => 'johndoe'], diff --git a/tests/functional/CronTaskTest.php b/tests/functional/CronTaskTest.php index 553489d4509..9f67cd92585 100644 --- a/tests/functional/CronTaskTest.php +++ b/tests/functional/CronTaskTest.php @@ -70,7 +70,7 @@ public function testCronTemp() foreach ($tmp_dir_iterator as $path) { if (basename($path) !== 'recent_file.txt') { // change the modification date of the file to make it considered as "not recent" - $this->assertTrue(touch($path, time() - (HOUR_TIMESTAMP * 2))); + $this->assertTrue(touch($path, time() - (HOUR_TIMESTAMP * 2)), 'Cannot set time on file ' . $path); } } diff --git a/tests/functional/DBTest.php b/tests/functional/DBTest.php index 0e4e3614479..8120cce5d65 100644 --- a/tests/functional/DBTest.php +++ b/tests/functional/DBTest.php @@ -149,19 +149,22 @@ public static function dataInsert() 'field' => 'value', 'other' => 'doe', ], - 'INSERT INTO `table` (`field`, `other`) VALUES (\'value\', \'doe\')', + 'INSERT INTO `table` (`field`, `other`) VALUES (?, ?)', + ['value', 'doe'], ], [ '`table`', [ '`field`' => 'value', '`other`' => 'doe', ], - 'INSERT INTO `table` (`field`, `other`) VALUES (\'value\', \'doe\')', + 'INSERT INTO `table` (`field`, `other`) VALUES (?, ?)', + ['value', 'doe'], ], [ 'table', [ 'field' => new QueryParam(), 'other' => new QueryParam(), ], 'INSERT INTO `table` (`field`, `other`) VALUES (?, ?)', + [], ], [ 'table', new QuerySubQuery([ 'SELECT' => ['id', 'name'], @@ -169,15 +172,18 @@ public static function dataInsert() 'WHERE' => ['NOT' => ['name' => null]], ]), 'INSERT INTO `table` (SELECT `id`, `name` FROM `other` WHERE NOT (`name` IS NULL))', + [], ], ]; } #[DataProvider('dataInsert')] - public function testBuildInsert($table, $values, $expected) + public function testBuildInsert(string $table, array|QuerySubQuery $values, string $expected_sql, array $expected_values) { $instance = new \DB(); - $this->assertSame($expected, $instance->buildInsert($table, $values)); + $insert = $instance->buildInsert($table, $values); + $this->assertSame($expected_sql, $insert->getSQL()); + $this->assertSame($expected_values, $insert->getValues()); } public static function dataUpdate() @@ -191,7 +197,8 @@ public static function dataUpdate() 'id' => 1, ], [], - 'UPDATE `table` SET `field` = \'value\', `other` = \'doe\' WHERE `id` = \'1\'', + 'UPDATE `table` SET `field` = ?, `other` = ? WHERE `id` = ?', + ['value', 'doe', 1], ], [ 'table', [ 'field' => 'value', @@ -199,7 +206,8 @@ public static function dataUpdate() 'id' => [1, 2], ], [], - 'UPDATE `table` SET `field` = \'value\' WHERE `id` IN (\'1\', \'2\')', + 'UPDATE `table` SET `field` = ? WHERE `id` IN (?, ?)', + ['value', 1, 2], ], [ 'table', [ 'field' => 'value', @@ -207,7 +215,8 @@ public static function dataUpdate() 'NOT' => ['id' => [1, 2]], ], [], - 'UPDATE `table` SET `field` = \'value\' WHERE NOT (`id` IN (\'1\', \'2\'))', + 'UPDATE `table` SET `field` = ? WHERE NOT (`id` IN (?, ?))', + ['value', 1, 2], ], [ 'table', [ 'field' => new QueryParam(), @@ -216,6 +225,7 @@ public static function dataUpdate() ], [], 'UPDATE `table` SET `field` = ? WHERE NOT (`id` IN (?, ?))', + [], ], [ 'table', [ 'field' => new QueryExpression(\DBmysql::quoteName('field') . ' + 1'), @@ -223,7 +233,8 @@ public static function dataUpdate() 'id' => [1, 2], ], [], - 'UPDATE `table` SET `field` = `field` + 1 WHERE `id` IN (\'1\', \'2\')', + 'UPDATE `table` SET `field` = `field` + 1 WHERE `id` IN (?, ?)', + [1, 2], ], [ 'table', [ 'field' => new QueryExpression(\DBmysql::quoteName('field') . ' + 1'), @@ -231,7 +242,8 @@ public static function dataUpdate() 'id' => [1, 2], ], [], - 'UPDATE `table` SET `field` = `field` + 1 WHERE `id` IN (\'1\', \'2\')', + 'UPDATE `table` SET `field` = `field` + 1 WHERE `id` IN (?, ?)', + [1, 2], ], [ 'table', [ 'field' => 'value', @@ -257,16 +269,19 @@ public static function dataUpdate() 'UPDATE `table`' . ' LEFT JOIN `another_table` ON (`table`.`foreign_id` = `another_table`.`id`)' . ' LEFT JOIN `table_3` ON (`another_table`.`some_id` = `table_3`.`id`)' - . ' SET `field` = \'value\' WHERE `id` IN (\'1\', \'2\')', + . ' SET `field` = ? WHERE `id` IN (?, ?)', + ['value', 1, 2], ], ]; } #[DataProvider('dataUpdate')] - public function testBuildUpdate($table, $values, $where, array $joins, $expected) + public function testBuildUpdate(string $table, array $values, array $where, array $joins, string $expected_sql, array $expected_values) { $instance = new \DB(); - $this->assertSame($expected, $instance->buildUpdate($table, $values, $where, $joins)); + $update = $instance->buildUpdate($table, $values, $where, $joins); + $this->assertSame($expected_sql, $update->getSQL()); + $this->assertSame($expected_values, $update->getValues()); } public function testBuildUpdateWException() @@ -284,25 +299,29 @@ public static function dataDelete() 'id' => 1, ], [], - 'DELETE `table` FROM `table` WHERE `id` = \'1\'', + 'DELETE `table` FROM `table` WHERE `id` = ?', + [1], ], [ 'table', [ 'id' => [1, 2], ], [], - 'DELETE `table` FROM `table` WHERE `id` IN (\'1\', \'2\')', + 'DELETE `table` FROM `table` WHERE `id` IN (?, ?)', + [1, 2], ], [ 'table', [ 'NOT' => ['id' => [1, 2]], ], [], - 'DELETE `table` FROM `table` WHERE NOT (`id` IN (\'1\', \'2\'))', + 'DELETE `table` FROM `table` WHERE NOT (`id` IN (?, ?))', + [1, 2], ], [ 'table', [ 'NOT' => ['id' => [new QueryParam(), new QueryParam()]], ], [], 'DELETE `table` FROM `table` WHERE NOT (`id` IN (?, ?))', + [], ], [ 'table', [ 'id' => 1, @@ -326,16 +345,19 @@ public static function dataDelete() 'DELETE `table` FROM `table`' . ' LEFT JOIN `another_table` ON (`table`.`foreign_id` = `another_table`.`id`)' . ' LEFT JOIN `table_3` ON (`another_table`.`some_id` = `table_3`.`id`)' - . ' WHERE `id` = \'1\'', + . ' WHERE `id` = ?', + [1], ], ]; } #[DataProvider('dataDelete')] - public function testBuildDelete($table, $where, array $joins, $expected) + public function testBuildDelete(string $table, array $where, array $joins, string $expected_sql, array $expected_values) { $instance = new \DB(); - $this->assertSame($expected, $instance->buildDelete($table, $where, $joins)); + $delete = $instance->buildDelete($table, $where, $joins); + $this->assertSame($expected_sql, $delete->getSQL()); + $this->assertSame($expected_values, $delete->getValues()); } public function testBuildDeleteWException() diff --git a/tests/functional/DBmysqlIteratorTest.php b/tests/functional/DBmysqlIteratorTest.php index b4775ae4706..50b2a56b18a 100644 --- a/tests/functional/DBmysqlIteratorTest.php +++ b/tests/functional/DBmysqlIteratorTest.php @@ -58,7 +58,7 @@ public function testSqlError(): void $this->expectExceptionObject( new \RuntimeException( - "MySQL query error: Table '{$DB->dbdefault}.fakeTable' doesn't exist (1146) in SQL query \"SELECT * FROM `fakeTable`\"." + "MySQL prepare error: Table '{$DB->dbdefault}.fakeTable' doesn't exist (1146) in SQL query \"SELECT * FROM `fakeTable`\"." ) ); $DB->request(['FROM' => 'fakeTable']); @@ -361,9 +361,10 @@ public function testJoins() ] ); $this->assertSame( - 'SELECT * FROM `foo` LEFT JOIN `bar` ON (`bar`.`id` = `foo`.`fk` OR `field` > \'20\')', + 'SELECT * FROM `foo` LEFT JOIN `bar` ON (`bar`.`id` = `foo`.`fk` OR `field` > ?)', $it->getSql() ); + $this->assertEquals([20], $it->getValues()); $it = $this->it->execute( [ @@ -381,9 +382,10 @@ public function testJoins() ] ); $this->assertSame( - 'SELECT * FROM `foo` LEFT JOIN `bar` ON (`bar`.`id` = `foo`.`fk` AND `field` = \'42\')', + 'SELECT * FROM `foo` LEFT JOIN `bar` ON (`bar`.`id` = `foo`.`fk` AND `field` = ?)', $it->getSql() ); + $this->assertEquals([42], $it->getValues()); //order in fkey should not matter $it = $this->it->execute( @@ -403,9 +405,10 @@ public function testJoins() ] ); $this->assertSame( - 'SELECT * FROM `foo` LEFT JOIN `bar` ON (`bar`.`id` = `foo`.`fk` AND `field` = \'42\')', + 'SELECT * FROM `foo` LEFT JOIN `bar` ON (`bar`.`id` = `foo`.`fk` AND `field` = ?)', $it->getSql() ); + $this->assertEquals([42], $it->getValues()); //condition set as associative array should work also $it = $this->it->execute( @@ -425,10 +428,10 @@ public function testJoins() ] ); $this->assertSame( - 'SELECT * FROM `foo` LEFT JOIN `bar` ON (`bar`.`id` = `foo`.`fk` AND `field` = \'42\')', + 'SELECT * FROM `foo` LEFT JOIN `bar` ON (`bar`.`id` = `foo`.`fk` AND `field` = ?)', $it->getSql() ); - + $this->assertEquals([42], $it->getValues()); //test derived table in JOIN statement $it = $this->it->execute( @@ -449,6 +452,7 @@ public function testJoins() 'SELECT * FROM `foo` LEFT JOIN (SELECT * FROM `bar`) AS `t2` ON (`t2`.`id` = `foo`.`fk`)', $it->getSql() ); + $this->assertEquals([], $it->getValues()); // join using query expression as first criterion $it = $this->it->execute( @@ -469,6 +473,7 @@ public function testJoins() 'SELECT * FROM `foo` LEFT JOIN `bar` ON (COALESCE(`bar.id`, 153) = `foo`.`fk`)', $it->getSql() ); + $this->assertEquals([], $it->getValues()); // join using query expression as second criterion $it = $this->it->execute( @@ -489,6 +494,7 @@ public function testJoins() 'SELECT * FROM `foo` LEFT JOIN `bar` ON (IFNULL(`bar.parent_id`, `bar.id`) = `foo`.`fk`)', $it->getSql() ); + $this->assertEquals([], $it->getValues()); // join using query expression for both criteria $it = $this->it->execute( @@ -509,6 +515,7 @@ public function testJoins() 'SELECT * FROM `foo` LEFT JOIN `bar` ON (COALESCE(`bar.id`, 153) = IFNULL(`bar.parent_id`, `bar.id`))', $it->getSql() ); + $this->assertEquals([], $it->getValues()); // join using subquery as first criterion $it = $this->it->execute( @@ -537,10 +544,11 @@ public function testJoins() 'SELECT * FROM `foo` LEFT JOIN `bar` ON (' . '`foo`.`fk`' . ' = ' - . '(SELECT `last_ticket_bar`.`id` FROM `bar` AS `last_ticket_bar` WHERE `last_ticket_bar`.`itemtype` = \'Ticket\' ORDER BY `last_ticket_bar`.`id` DESC LIMIT 1)' + . '(SELECT `last_ticket_bar`.`id` FROM `bar` AS `last_ticket_bar` WHERE `last_ticket_bar`.`itemtype` = ? ORDER BY `last_ticket_bar`.`id` DESC LIMIT 1)' . ')', $it->getSql() ); + $this->assertEquals(['Ticket'], $it->getValues()); // join using subquery as second criterion $it = $this->it->execute( @@ -567,12 +575,13 @@ public function testJoins() ); $this->assertSame( 'SELECT * FROM `foo` LEFT JOIN `bar` ON (' - . '(SELECT `last_ticket_bar`.`id` FROM `bar` AS `last_ticket_bar` WHERE `last_ticket_bar`.`itemtype` = \'Ticket\' ORDER BY `last_ticket_bar`.`id` DESC LIMIT 1)' + . '(SELECT `last_ticket_bar`.`id` FROM `bar` AS `last_ticket_bar` WHERE `last_ticket_bar`.`itemtype` = ? ORDER BY `last_ticket_bar`.`id` DESC LIMIT 1)' . ' = ' . '`foo`.`fk`' . ')', $it->getSql() ); + $this->assertEquals(['Ticket'], $it->getValues()); // join using subquery for both criteria $it = $this->it->execute( @@ -607,12 +616,13 @@ public function testJoins() ); $this->assertSame( 'SELECT * FROM `foo` LEFT JOIN `bar` ON (' - . '(SELECT `last_ticket_bar`.`id` FROM `bar` AS `last_ticket_bar` WHERE `last_ticket_bar`.`itemtype` = \'Ticket\' ORDER BY `last_ticket_bar`.`id` DESC LIMIT 1)' + . '(SELECT `last_ticket_bar`.`id` FROM `bar` AS `last_ticket_bar` WHERE `last_ticket_bar`.`itemtype` = ? ORDER BY `last_ticket_bar`.`id` DESC LIMIT 1)' . ' = ' - . '(SELECT `first_ticket_bar`.`id` FROM `bar` AS `first_ticket_bar` WHERE `last_ticket_bar`.`itemtype` = \'Ticket\' ORDER BY `first_ticket_bar`.`id` ASC LIMIT 1)' + . '(SELECT `first_ticket_bar`.`id` FROM `bar` AS `first_ticket_bar` WHERE `last_ticket_bar`.`itemtype` = ? ORDER BY `first_ticket_bar`.`id` ASC LIMIT 1)' . ')', $it->getSql() ); + $this->assertEquals(['Ticket', 'Ticket'], $it->getValues()); // using a unique query expression as criterion $it = $this->it->execute( @@ -630,7 +640,7 @@ public function testJoins() 'SELECT * FROM `foo` LEFT JOIN `bar` ON (COALESCE(`bar.id`, 153) = IFNULL(`bar.parent_id`, `bar.id`))', $it->getSql() ); - + $this->assertEquals([], $it->getValues()); } public function testBadJoin() @@ -668,16 +678,20 @@ public function testAnalyseJoins() public function testHaving() { $it = $this->it->execute(['FROM' => 'foo', 'HAVING' => ['bar' => true]]); - $this->assertSame("SELECT * FROM `foo` HAVING `bar` = '1'", $it->getSql()); + $this->assertSame("SELECT * FROM `foo` HAVING `bar` = ?", $it->getSql()); + $this->assertEquals([1], $it->getValues()); $it = $this->it->execute(['FROM' => 'foo', 'HAVING' => ['bar' => false]]); - $this->assertSame("SELECT * FROM `foo` HAVING `bar` = '0'", $it->getSql()); + $this->assertSame("SELECT * FROM `foo` HAVING `bar` = ?", $it->getSql()); + $this->assertEquals([0], $it->getValues()); $it = $this->it->execute(['FROM' => 'foo', 'HAVING' => ['bar' => 1]]); - $this->assertSame('SELECT * FROM `foo` HAVING `bar` = \'1\'', $it->getSql()); + $this->assertSame('SELECT * FROM `foo` HAVING `bar` = ?', $it->getSql()); + $this->assertEquals([1], $it->getValues()); $it = $this->it->execute(['FROM' => 'foo', 'HAVING' => ['bar' => 23.5579]]); - $this->assertSame("SELECT * FROM `foo` HAVING `bar` = '23.5579'", $it->getSql()); + $this->assertSame("SELECT * FROM `foo` HAVING `bar` = ?", $it->getSql()); + $this->assertEquals([23.5579], $it->getValues()); $stringable_object = new class ("L'Appel de Cthulhu") { public function __construct(private string $val) {} @@ -687,41 +701,53 @@ public function __toString() return $this->val; } }; + $it = $this->it->execute(['FROM' => 'foo', 'HAVING' => ['bar' => $stringable_object]]); - $this->assertSame("SELECT * FROM `foo` HAVING `bar` = 'L\'Appel de Cthulhu'", $it->getSql()); + $this->assertSame("SELECT * FROM `foo` HAVING `bar` = ?", $it->getSql()); + $this->assertEquals(["L'Appel de Cthulhu"], $it->getValues()); $it = $this->it->execute(['FROM' => 'foo', 'HAVING' => ['bar' => ['>', 0]]]); - $this->assertSame('SELECT * FROM `foo` HAVING `bar` > \'0\'', $it->getSql()); + $this->assertSame('SELECT * FROM `foo` HAVING `bar` > ?', $it->getSql()); + $this->assertEquals([0], $it->getValues()); } public function testOperators() { $it = $this->it->execute(['FROM' => 'foo', 'WHERE' => ['a' => 1]]); - $this->assertSame('SELECT * FROM `foo` WHERE `a` = \'1\'', $it->getSql()); + $this->assertSame('SELECT * FROM `foo` WHERE `a` = ?', $it->getSql()); + $this->assertEquals([1], $it->getValues()); $it = $this->it->execute(['FROM' => 'foo', 'WHERE' => ['a' => ['=', 1]]]); - $this->assertSame('SELECT * FROM `foo` WHERE `a` = \'1\'', $it->getSql()); + $this->assertSame('SELECT * FROM `foo` WHERE `a` = ?', $it->getSql()); + $this->assertEquals([1], $it->getValues()); $it = $this->it->execute(['FROM' => 'foo', 'WHERE' => ['a' => ['>', 1]]]); - $this->assertSame('SELECT * FROM `foo` WHERE `a` > \'1\'', $it->getSql()); + $this->assertSame('SELECT * FROM `foo` WHERE `a` > ?', $it->getSql()); + $this->assertEquals([1], $it->getValues()); $it = $this->it->execute(['FROM' => 'foo', 'WHERE' => ['a' => ['LIKE', '%bar%']]]); - $this->assertSame('SELECT * FROM `foo` WHERE `a` LIKE \'%bar%\'', $it->getSql()); + $this->assertSame('SELECT * FROM `foo` WHERE `a` LIKE ?', $it->getSql()); + $this->assertEquals(['%bar%'], $it->getValues()); $it = $this->it->execute(['FROM' => 'foo', 'WHERE' => ['NOT' => ['a' => ['LIKE', '%bar%']]]]); - $this->assertSame('SELECT * FROM `foo` WHERE NOT (`a` LIKE \'%bar%\')', $it->getSql()); + $this->assertSame('SELECT * FROM `foo` WHERE NOT (`a` LIKE ?)', $it->getSql()); + $this->assertEquals(['%bar%'], $it->getValues()); $it = $this->it->execute(['FROM' => 'foo', 'WHERE' => ['a' => ['NOT LIKE', '%bar%']]]); - $this->assertSame('SELECT * FROM `foo` WHERE `a` NOT LIKE \'%bar%\'', $it->getSql()); + $this->assertSame('SELECT * FROM `foo` WHERE `a` NOT LIKE ?', $it->getSql()); + $this->assertEquals(['%bar%'], $it->getValues()); $it = $this->it->execute(['FROM' => 'foo', 'WHERE' => ['a' => ['<>', 1]]]); - $this->assertSame('SELECT * FROM `foo` WHERE `a` <> \'1\'', $it->getSql()); + $this->assertSame('SELECT * FROM `foo` WHERE `a` <> ?', $it->getSql()); + $this->assertEquals([1], $it->getValues()); $it = $this->it->execute(['FROM' => 'foo', 'WHERE' => ['a' => ['&', 1]]]); - $this->assertSame('SELECT * FROM `foo` WHERE `a` & \'1\'', $it->getSql()); + $this->assertSame('SELECT * FROM `foo` WHERE `a` & ?', $it->getSql()); + $this->assertEquals([1], $it->getValues()); $it = $this->it->execute(['FROM' => 'foo', 'WHERE' => ['a' => ['|', 1]]]); - $this->assertSame('SELECT * FROM `foo` WHERE `a` | \'1\'', $it->getSql()); + $this->assertSame('SELECT * FROM `foo` WHERE `a` | ?', $it->getSql()); + $this->assertEquals([1], $it->getValues()); } @@ -737,25 +763,31 @@ public function testWhere() $this->assertSame('SELECT * FROM `foo` WHERE `bar` IS NULL', $it->getSql()); $it = $this->it->execute(['FROM' => 'foo', 'WHERE' => ['bar' => 1]]); - $this->assertSame('SELECT * FROM `foo` WHERE `bar` = \'1\'', $it->getSql()); + $this->assertSame('SELECT * FROM `foo` WHERE `bar` = ?', $it->getSql()); + $this->assertEquals([1], $it->getValues()); $it = $this->it->execute(['FROM' => 'foo', 'WHERE' => ['bar' => 1.1549]]); - $this->assertSame("SELECT * FROM `foo` WHERE `bar` = '1.1549'", $it->getSql()); + $this->assertSame("SELECT * FROM `foo` WHERE `bar` = ?", $it->getSql()); + $this->assertEquals([1.1549], $it->getValues()); $it = $this->it->execute(['FROM' => 'foo', 'WHERE' => ['bar' => [1, 2, 4]]]); - $this->assertSame("SELECT * FROM `foo` WHERE `bar` IN ('1', '2', '4')", $it->getSql()); + $this->assertSame("SELECT * FROM `foo` WHERE `bar` IN (?, ?, ?)", $it->getSql()); + $this->assertEquals([1, 2, 4], $it->getValues()); $it = $this->it->execute(['FROM' => 'foo', 'WHERE' => ['bar' => ['a', 'b', 'c']]]); - $this->assertSame("SELECT * FROM `foo` WHERE `bar` IN ('a', 'b', 'c')", $it->getSql()); + $this->assertSame("SELECT * FROM `foo` WHERE `bar` IN (?, ?, ?)", $it->getSql()); + $this->assertEquals(['a', 'b', 'c'], $it->getValues()); $it = $this->it->execute(['FROM' => 'foo', 'WHERE' => ['bar' => 'val']]); - $this->assertSame("SELECT * FROM `foo` WHERE `bar` = 'val'", $it->getSql()); + $this->assertSame("SELECT * FROM `foo` WHERE `bar` = ?", $it->getSql()); + $this->assertEquals(['val'], $it->getValues()); $it = $this->it->execute(['FROM' => 'foo', 'WHERE' => ['bar' => new QueryExpression('`field`')]]); $this->assertSame('SELECT * FROM `foo` WHERE `bar` = `field`', $it->getSql()); $it = $this->it->execute(['FROM' => 'foo', 'WHERE' => ['bar' => '?']]); - $this->assertSame('SELECT * FROM `foo` WHERE `bar` = \'?\'', $it->getSql()); + $this->assertSame('SELECT * FROM `foo` WHERE `bar` = ?', $it->getSql()); + $this->assertEquals(['?'], $it->getValues()); $it = $this->it->execute(['FROM' => 'foo', 'WHERE' => ['bar' => new QueryParam()]]); $this->assertSame('SELECT * FROM `foo` WHERE `bar` = ?', $it->getSql()); @@ -769,7 +801,8 @@ public function __toString() } }; $it = $this->it->execute(['FROM' => 'foo', 'WHERE' => ['bar' => $stringable_object]]); - $this->assertSame("SELECT * FROM `foo` WHERE `bar` = 'L\'Appel de Cthulhu'", $it->getSql()); + $this->assertSame("SELECT * FROM `foo` WHERE `bar` = ?", $it->getSql()); + $this->assertEquals(["L'Appel de Cthulhu"], $it->getValues()); } public function testEmptyIn(): void @@ -790,16 +823,20 @@ public function testFkey() $this->assertSame('SELECT * FROM `foo`, `bar` WHERE `foo`.`id` = `bar`.`fk`', $it->getSql()); $it = $this->it->execute(['FROM' => ['foo', 'bar'], 'WHERE' => ['FKEY' => ['`foo`' => 'id', 'bar' => '`fk`', ['AND' => ['baz' => true]]]]]); - $this->assertSame("SELECT * FROM `foo`, `bar` WHERE `foo`.`id` = `bar`.`fk` AND `baz` = '1'", $it->getSql()); + $this->assertSame("SELECT * FROM `foo`, `bar` WHERE `foo`.`id` = `bar`.`fk` AND `baz` = ?", $it->getSql()); + $this->assertEquals([1], $it->getValues()); $it = $this->it->execute(['FROM' => ['foo', 'bar'], 'WHERE' => ['FKEY' => ['`foo`' => 'id', 'bar' => '`fk`', ['AND' => ['baz' => false]]]]]); - $this->assertSame("SELECT * FROM `foo`, `bar` WHERE `foo`.`id` = `bar`.`fk` AND `baz` = '0'", $it->getSql()); + $this->assertSame("SELECT * FROM `foo`, `bar` WHERE `foo`.`id` = `bar`.`fk` AND `baz` = ?", $it->getSql()); + $this->assertEquals([0], $it->getValues()); $it = $this->it->execute(['FROM' => ['foo', 'bar'], 'WHERE' => ['FKEY' => ['`foo`' => 'id', 'bar' => '`fk`', ['AND' => ['baz' => 150]]]]]); - $this->assertSame("SELECT * FROM `foo`, `bar` WHERE `foo`.`id` = `bar`.`fk` AND `baz` = '150'", $it->getSql()); + $this->assertSame("SELECT * FROM `foo`, `bar` WHERE `foo`.`id` = `bar`.`fk` AND `baz` = ?", $it->getSql()); + $this->assertEquals([150], $it->getValues()); $it = $this->it->execute(['FROM' => ['foo', 'bar'], 'WHERE' => ['FKEY' => ['`foo`' => 'id', 'bar' => '`fk`', ['AND' => ['baz' => 23.5579]]]]]); - $this->assertSame("SELECT * FROM `foo`, `bar` WHERE `foo`.`id` = `bar`.`fk` AND `baz` = '23.5579'", $it->getSql()); + $this->assertSame("SELECT * FROM `foo`, `bar` WHERE `foo`.`id` = `bar`.`fk` AND `baz` = ?", $it->getSql()); + $this->assertEquals([23.5579], $it->getValues()); $stringable_object = new class ("L'Appel de Cthulhu") { public function __construct(private string $val) {} @@ -810,7 +847,8 @@ public function __toString() } }; $it = $this->it->execute(['FROM' => ['foo', 'bar'], 'WHERE' => ['FKEY' => ['`foo`' => 'id', 'bar' => '`fk`', ['AND' => ['baz' => $stringable_object]]]]]); - $this->assertSame("SELECT * FROM `foo`, `bar` WHERE `foo`.`id` = `bar`.`fk` AND `baz` = 'L\'Appel de Cthulhu'", $it->getSql()); + $this->assertSame("SELECT * FROM `foo`, `bar` WHERE `foo`.`id` = `bar`.`fk` AND `baz` = ?", $it->getSql()); + $this->assertEquals(["L'Appel de Cthulhu"], $it->getValues()); } public function testGroupBy() @@ -860,16 +898,20 @@ public function testRange() public function testLogical() { $it = $this->it->execute(['FROM' => 'foo', 'WHERE' => [['a' => 1, 'b' => 2]]]); - $this->assertSame('SELECT * FROM `foo` WHERE (`a` = \'1\' AND `b` = \'2\')', $it->getSql()); + $this->assertSame('SELECT * FROM `foo` WHERE (`a` = ? AND `b` = ?)', $it->getSql()); + $this->assertEquals([1, 2], $it->getValues()); $it = $this->it->execute(['FROM' => 'foo', 'WHERE' => ['AND' => ['a' => 1, 'b' => 2]]]); - $this->assertSame('SELECT * FROM `foo` WHERE (`a` = \'1\' AND `b` = \'2\')', $it->getSql()); + $this->assertSame('SELECT * FROM `foo` WHERE (`a` = ? AND `b` = ?)', $it->getSql()); + $this->assertEquals([1, 2], $it->getValues()); $it = $this->it->execute(['FROM' => 'foo', 'WHERE' => ['OR' => ['a' => 1, 'b' => 2]]]); - $this->assertSame('SELECT * FROM `foo` WHERE (`a` = \'1\' OR `b` = \'2\')', $it->getSql()); + $this->assertSame('SELECT * FROM `foo` WHERE (`a` = ? OR `b` = ?)', $it->getSql()); + $this->assertEquals([1, 2], $it->getValues()); $it = $this->it->execute(['FROM' => 'foo', 'WHERE' => ['NOT' => ['a' => 1, 'b' => 2]]]); - $this->assertSame('SELECT * FROM `foo` WHERE NOT (`a` = \'1\' AND `b` = \'2\')', $it->getSql()); + $this->assertSame('SELECT * FROM `foo` WHERE NOT (`a` = ? AND `b` = ?)', $it->getSql()); + $this->assertEquals([1, 2], $it->getValues()); $crit = [ 'FROM' => 'foo', @@ -886,9 +928,10 @@ public function testLogical() ], ], ]; - $sql = "SELECT * FROM `foo` WHERE ((`items_id` = '15' AND `itemtype` = 'Computer') OR (`items_id` = '3' AND `itemtype` = 'Document'))"; + $sql = "SELECT * FROM `foo` WHERE ((`items_id` = ? AND `itemtype` = ?) OR (`items_id` = ? AND `itemtype` = ?))"; $it = $this->it->execute($crit); $this->assertSame($sql, $it->getSql()); + $this->assertEquals([15, 'Computer', 3, 'Document'], $it->getValues()); $crit = [ 'FROM' => 'foo', @@ -906,9 +949,10 @@ public function testLogical() ], ], ]; - $sql = "SELECT * FROM `foo` WHERE `a` = '1' AND (`b` = '2' OR NOT (`c` IN ('2', '3') AND (`d` = '4' AND `e` = '5')))"; + $sql = "SELECT * FROM `foo` WHERE `a` = ? AND (`b` = ? OR NOT (`c` IN (?, ?) AND (`d` = ? AND `e` = ?)))"; $it = $this->it->execute($crit); $this->assertSame($sql, $it->getSql()); + $this->assertEquals([1, 2, 2, 3, 4, 5], $it->getValues()); $crit = [ 'FROM' => 'foo', @@ -918,7 +962,8 @@ public function testLogical() ], ]; $it = $this->it->execute($crit); - $this->assertSame("SELECT * FROM `foo` WHERE `bar` = 'baz' AND ((SELECT COUNT(*) FROM xyz) = '5')", $it->getSql()); + $this->assertSame("SELECT * FROM `foo` WHERE `bar` = ? AND ((SELECT COUNT(*) FROM xyz) = ?)", $it->getSql()); + $this->assertEquals(['baz', 5], $it->getValues()); $crit = [ 'FROM' => 'foo', @@ -928,7 +973,8 @@ public function testLogical() ], ]; $it = $this->it->execute($crit); - $this->assertSame("SELECT * FROM `foo` WHERE `bar` = 'baz' AND ((SELECT COUNT(*) FROM xyz) > '2')", $it->getSql()); + $this->assertSame("SELECT * FROM `foo` WHERE `bar` = ? AND ((SELECT COUNT(*) FROM xyz) > ?)", $it->getSql()); + $this->assertEquals(['baz', 2], $it->getValues()); $crit = [ 'FROM' => 'foo', @@ -938,7 +984,8 @@ public function testLogical() ], ]; $it = $this->it->execute($crit); - $this->assertSame("SELECT * FROM `foo` WHERE `bar` = 'baz' AND ((SELECT COUNT(*) FROM xyz) IN ('3', '4'))", $it->getSql()); + $this->assertSame("SELECT * FROM `foo` WHERE `bar` = ? AND ((SELECT COUNT(*) FROM xyz) IN (?, ?))", $it->getSql()); + $this->assertEquals(['baz', 3, 4], $it->getValues()); } @@ -949,9 +996,10 @@ public function testModern() 'FROM' => 'foo', 'WHERE' => ['c' => 1], ]; - $sql = "SELECT `a`, `b` FROM `foo` WHERE `c` = '1'"; + $sql = "SELECT `a`, `b` FROM `foo` WHERE `c` = ?"; $it = $this->it->execute($req); $this->assertSame($sql, $it->getSql()); + $this->assertEquals([1], $it->getValues()); } @@ -1031,10 +1079,11 @@ public function testExpression() public function testSubQuery() { $crit = ['SELECT' => 'id', 'FROM' => 'baz', 'WHERE' => ['z' => 'f']]; - $raw_subq = "(SELECT `id` FROM `baz` WHERE `z` = 'f')"; + $raw_subq = "(SELECT `id` FROM `baz` WHERE `z` = ?)"; $sub_query = new QuerySubQuery($crit); $this->assertSame($raw_subq, $sub_query->getQuery()); + $this->assertEquals(['f'], $sub_query->getValues()); $it = $this->it->execute(['FROM' => 'foo', 'WHERE' => ['bar' => $sub_query]]); $this->assertSame( @@ -1122,7 +1171,6 @@ public function testComplexUnionQuery() { $fk = \Ticket::getForeignKeyField(); - $users_table = \User::getTable(); $users_table = 'glpi_ticket_users'; $groups_table = 'glpi_groups_tickets'; @@ -1173,14 +1221,18 @@ public function testComplexUnionQuery() . " FROM ((SELECT `usr`.`id` AS `users_id`, `tu`.`type` AS `type`" . " FROM `$users_table` AS `tu`" . " LEFT JOIN `glpi_users` AS `usr` ON (`tu`.`users_id` = `usr`.`id`)" - . " WHERE `tu`.`$fk` = '42')" + . " WHERE `tu`.`$fk` = ?)" . " UNION ALL" . " (SELECT `usr`.`id` AS `users_id`, `gt`.`type` AS `type`" . " FROM `$groups_table` AS `gt`" . " LEFT JOIN `glpi_groups_users` AS `gu` ON (`gu`.`groups_id` = `gt`.`groups_id`)" . " LEFT JOIN `glpi_users` AS `usr` ON (`gu`.`users_id` = `usr`.`id`)" - . " WHERE `gt`.`$fk` = '42')" + . " WHERE `gt`.`$fk` = ?)" . ") AS `allactors`"; + $parameters = [ + 42, // tu.fk + 42, // gt.fk + ]; $union = new QueryUnion([$subquery1, $subquery2], false, 'allactors'); $it = $this->it->execute([ @@ -1192,6 +1244,7 @@ public function testComplexUnionQuery() 'FROM' => $union, ]); $this->assertSame($raw_query, $it->getSql()); + $this->assertEquals($parameters, $it->getValues()); } public function testComplexUnionQueryAgain() @@ -1200,6 +1253,7 @@ public function testComplexUnionQueryAgain() //Old build way $queries = []; + $parameters = []; foreach ($CFG_GLPI["networkport_types"] as $itemtype) { $table = getTableForItemType($itemtype); @@ -1218,15 +1272,20 @@ public function testComplexUnionQueryAgain() '$itemtype' AS `item_type` FROM `glpi_ipaddresses_ipnetworks` AS `LINK` INNER JOIN `glpi_ipaddresses` AS `ADDR` ON (`ADDR`.`id` = `LINK`.`ipaddresses_id` - AND `ADDR`.`itemtype` = 'NetworkName' - AND `ADDR`.`is_deleted` = '0') + AND `ADDR`.`itemtype` = ? + AND `ADDR`.`is_deleted` = ?) INNER JOIN `glpi_networknames` AS `NAME` ON (`NAME`.`id` = `ADDR`.`items_id` - AND `NAME`.`itemtype` = 'NetworkPort') + AND `NAME`.`itemtype` = ?) INNER JOIN `glpi_networkports` AS `PORT` ON (`NAME`.`items_id` = `PORT`.`id` - AND `PORT`.`itemtype` = '$itemtype') + AND `PORT`.`itemtype` = ?) INNER JOIN `$table` AS `ITEM` ON (`ITEM`.`id` = `PORT`.`items_id`) LEFT JOIN `glpi_entities` ON (`ADDR`.`entities_id` = `glpi_entities`.`id`) - WHERE `LINK`.`ipnetworks_id` = '42')"; + WHERE `LINK`.`ipnetworks_id` = ?)"; + $parameters[] = 'NetworkName'; //ADDR.itemtype + $parameters[] = 0; // is_deleted + $parameters[] = 'NetworkPort'; // NAME.itemtype + $parameters[] = $itemtype; //PORT.itemtype + $parameters[] = 42; // LINK/.ipnetworks_id } $queries[] = "(SELECT `ADDR`.`binary_0` AS `binary_0`, @@ -1244,16 +1303,21 @@ public function testComplexUnionQueryAgain() NULL AS `item_type` FROM `glpi_ipaddresses_ipnetworks` AS `LINK` INNER JOIN `glpi_ipaddresses` AS `ADDR` ON (`ADDR`.`id` = `LINK`.`ipaddresses_id` - AND `ADDR`.`itemtype` = 'NetworkName' - AND `ADDR`.`is_deleted` = '0') + AND `ADDR`.`itemtype` = ? + AND `ADDR`.`is_deleted` = ?) INNER JOIN `glpi_networknames` AS `NAME` ON (`NAME`.`id` = `ADDR`.`items_id` - AND `NAME`.`itemtype` = 'NetworkPort') + AND `NAME`.`itemtype` = ?) INNER JOIN `glpi_networkports` AS `PORT` ON (`NAME`.`items_id` = `PORT`.`id` AND NOT (`PORT`.`itemtype` - IN ('" . implode("', '", $CFG_GLPI["networkport_types"]) . "'))) + IN (" . str_repeat('?, ', count($CFG_GLPI["networkport_types"]) - 1) . '?' . "))) LEFT JOIN `glpi_entities` ON (`ADDR`.`entities_id` = `glpi_entities`.`id`) - WHERE `LINK`.`ipnetworks_id` = '42')"; + WHERE `LINK`.`ipnetworks_id` = ?)"; + $parameters[] = 'NetworkName'; //ADDR.itemtype + $parameters[] = 0; // is_deleted + $parameters[] = 'NetworkPort'; // NAME.itemtype + $parameters = array_merge($parameters, $CFG_GLPI["networkport_types"]); // PORT.itemtype + $parameters[] = 42; // LINK.ipnetworks_id $queries[] = "(SELECT `ADDR`.`binary_0` AS `binary_0`, `ADDR`.`binary_1` AS `binary_1`, @@ -1270,12 +1334,16 @@ public function testComplexUnionQueryAgain() NULL AS `item_type` FROM `glpi_ipaddresses_ipnetworks` AS `LINK` INNER JOIN `glpi_ipaddresses` AS `ADDR` ON (`ADDR`.`id` = `LINK`.`ipaddresses_id` - AND `ADDR`.`itemtype` = 'NetworkName' - AND `ADDR`.`is_deleted` = '0') + AND `ADDR`.`itemtype` = ? + AND `ADDR`.`is_deleted` = ?) INNER JOIN `glpi_networknames` AS `NAME` ON (`NAME`.`id` = `ADDR`.`items_id` - AND `NAME`.`itemtype` != 'NetworkPort') + AND `NAME`.`itemtype` != ?) LEFT JOIN `glpi_entities` ON (`ADDR`.`entities_id` = `glpi_entities`.`id`) - WHERE `LINK`.`ipnetworks_id` = '42')"; + WHERE `LINK`.`ipnetworks_id` = ?)"; + $parameters[] = 'NetworkName'; //ADDR.itemtype + $parameters[] = 0; // is_deleted + $parameters[] = 'NetworkPort'; // NAME.itemtype + $parameters[] = 42; // LINK.ipnetworks_id $queries[] = "(SELECT `ADDR`.`binary_0` AS `binary_0`, `ADDR`.`binary_1` AS `binary_1`, @@ -1292,10 +1360,13 @@ public function testComplexUnionQueryAgain() NULL AS `item_type` FROM `glpi_ipaddresses_ipnetworks` AS `LINK` INNER JOIN `glpi_ipaddresses` AS `ADDR` ON (`ADDR`.`id` = `LINK`.`ipaddresses_id` - AND `ADDR`.`itemtype` != 'NetworkName' - AND `ADDR`.`is_deleted` = '0') + AND `ADDR`.`itemtype` != ? + AND `ADDR`.`is_deleted` = ?) LEFT JOIN `glpi_entities` ON (`ADDR`.`entities_id` = `glpi_entities`.`id`) - WHERE `LINK`.`ipnetworks_id` = '42')"; + WHERE `LINK`.`ipnetworks_id` = ?)"; + $parameters[] = 'NetworkName'; //ADDR.itemtype + $parameters[] = 0; // is_deleted + $parameters[] = 42; // LINK.ipnetworks_id $union_raw_query = '(' . preg_replace('/\s+/', ' ', implode(' UNION ALL ', $queries)) . ')'; $raw_query = 'SELECT * FROM ' . $union_raw_query . ' AS `union_' . md5($union_raw_query) . '`'; @@ -1452,6 +1523,7 @@ public function testComplexUnionQueryAgain() $it = $this->it->execute($criteria); $this->assertSame($raw_query, $it->getSql()); + $this->assertEquals($parameters, $it->getValues()); } public function testAnalyseCrit() @@ -1635,35 +1707,46 @@ private function getUsersFakeTable(): QueryExpression public function testInCriteria() { global $DB; - $iterator = new \DBmysqlIterator($DB); - $to_sql_array = static function ($values) use ($DB) { - $str = '('; - foreach ($values as $value) { - $str .= $DB->quoteValue($value) . ', '; - } - return rtrim($str, ', ') . ')'; + $to_parameters_array = static function ($values) { + return sprintf( + '(%s)', + implode( + ', ', + array_fill( + 0, + count($values), + '?' + ) + ) + ); }; - // Reguar IN + // Regular IN $criteria = [ 'id' => [1, 2, 3], ]; - $expected = $DB::quoteName('id') . " IN " . $to_sql_array($criteria['id']); + $expected = $DB::quoteName('id') . " IN " . $to_parameters_array($criteria['id']); + $iterator = new \DBmysqlIterator($DB); $this->assertEquals($expected, $iterator->analyseCrit($criteria)); + $this->assertArraysEqualRecursive($criteria['id'], $iterator->getValues()); // Explicit IN (array form) $criteria = [ 'id' => ['IN', [1, 2, 3]], ]; - $expected = $DB::quoteName('id') . " IN " . $to_sql_array($criteria['id'][1]); + $expected = $DB::quoteName('id') . " IN " . $to_parameters_array($criteria['id'][1]); + $iterator = new \DBmysqlIterator($DB); $this->assertEquals($expected, $iterator->analyseCrit($criteria)); + $this->assertArraysEqualRecursive($criteria['id'][1], $iterator->getValues()); // Explicit NOT IN (array form) $criteria = [ 'id' => ['NOT IN', [1, 2, 3]], ]; - $expected = $DB::quoteName('id') . " NOT IN " . $to_sql_array($criteria['id'][1]); + $expected = $DB::quoteName('id') . " NOT IN " . $to_parameters_array($criteria['id'][1]); + $iterator = new \DBmysqlIterator($DB); $this->assertEquals($expected, $iterator->analyseCrit($criteria)); + $this->assertArraysEqualRecursive($criteria['id'][1], $iterator->getValues()); } public static function resultProvider(): iterable @@ -1757,12 +1840,16 @@ public function testAutoUnsanitize(array $db_data, array $result): void $mysqli_result = $this->createMock(\mysqli_result::class); $mysqli_result->method('fetch_assoc')->willReturn($db_data); $mysqli_result->method('data_seek')->willReturn(true); + $mysqli_stmt = $this->createMock(\mysqli_stmt::class); + $mysqli_stmt->method('execute')->willReturn(true); + $mysqli_stmt->method('get_result')->willReturn($mysqli_result); $db = $this->getMockBuilder(\DBMysql::class) - ->onlyMethods(['connect', 'doQuery', 'numrows']) + ->onlyMethods(['connect', 'doQuery', 'numrows', 'prepare']) ->getMock(); $db->method('doQuery')->willReturn($mysqli_result); $db->method('numrows')->willReturn(1); + $db->method('prepare')->willReturn($mysqli_stmt); // Check result with active unsanitization $db->setMustUnsanitizeData(true); diff --git a/tests/functional/MigrationTest.php b/tests/functional/MigrationTest.php index a9744da81f2..bc927bd4940 100644 --- a/tests/functional/MigrationTest.php +++ b/tests/functional/MigrationTest.php @@ -37,14 +37,18 @@ use ArrayIterator; use Computer; use CronTask; +use DBmysql; use Glpi\DBAL\QuerySubQuery; use Glpi\Progress\AbstractProgressIndicator; use Glpi\Socket; use Glpi\Tests\DbTestCase; use LogicException; use Migration; +use mysqli_stmt; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; +use ReflectionProperty; +use RuntimeException; class MigrationTest extends DbTestCase { @@ -189,9 +193,9 @@ public function testPrePostQueries() $migration->executeMigration(); $this->assertEquals([ - 'UPDATE pre_table SET mfield = "myvalue"', - 'UPDATE post_table SET mfield = "myvalue"', - 'UPDATE post_otable SET ofield = "myvalue"', + ['sql' => 'UPDATE pre_table SET mfield = "myvalue"'], + ['sql' => 'UPDATE post_table SET mfield = "myvalue"'], + ['sql' => 'UPDATE post_otable SET ofield = "myvalue"'], ], $migration->getMockedQueries()); } @@ -216,9 +220,18 @@ public function testAddConfig() ]); $migration->executeMigration(); $core_queries = [ - 'SELECT * FROM `glpi_configs` WHERE `context` = \'core\' AND `name` IN (\'one\', \'two\')', - 'INSERT INTO `glpi_configs` (`context`, `name`, `value`) VALUES (\'core\', \'one\', \'key\')', - 'INSERT INTO `glpi_configs` (`context`, `name`, `value`) VALUES (\'core\', \'two\', \'value\')', + [ + 'sql' => 'SELECT * FROM `glpi_configs` WHERE `context` = ? AND `name` IN (?, ?)', + 'values' => ['core', 'one', 'two'], + ], + [ + 'sql' => 'INSERT INTO `glpi_configs` (`context`, `name`, `value`) VALUES (?, ?, ?)', + 'values' => ['core', 'one', 'key'], + ], + [ + 'sql' => 'INSERT INTO `glpi_configs` (`context`, `name`, `value`) VALUES (?, ?, ?)', + 'values' => ['core', 'two', 'value'], + ], ]; $this->assertEquals($core_queries, $migration->getMockedQueries(), print_r($migration->getMockedQueries(), true)); @@ -231,9 +244,18 @@ public function testAddConfig() $migration->executeMigration(); $this->assertEquals([ - 'SELECT * FROM `glpi_configs` WHERE `context` = \'test-context\' AND `name` IN (\'one\', \'two\')', - 'INSERT INTO `glpi_configs` (`context`, `name`, `value`) VALUES (\'test-context\', \'one\', \'key\')', - 'INSERT INTO `glpi_configs` (`context`, `name`, `value`) VALUES (\'test-context\', \'two\', \'value\')', + [ + 'sql' => 'SELECT * FROM `glpi_configs` WHERE `context` = ? AND `name` IN (?, ?)', + 'values' => ['test-context', 'one', 'two'], + ], + [ + 'sql' => 'INSERT INTO `glpi_configs` (`context`, `name`, `value`) VALUES (?, ?, ?)', + 'values' => ['test-context', 'one', 'key'], + ], + [ + 'sql' => 'INSERT INTO `glpi_configs` (`context`, `name`, `value`) VALUES (?, ?, ?)', + 'values' => ['test-context', 'two', 'value'], + ], ], $migration->getMockedQueries()); //test with one existing value => only new key should be inserted @@ -271,7 +293,10 @@ public function testAddConfig() ]); $migration->executeMigration(); $this->assertEquals([ - 0 => 'INSERT INTO `glpi_configs` (`context`, `name`, `value`) VALUES (\'core\', \'two\', \'value\')', + [ + 'sql' => 'INSERT INTO `glpi_configs` (`context`, `name`, `value`) VALUES (?, ?, ?)', + 'values' => ['core', 'two', 'value'], + ], ], $migration->getMockedQueries()); } @@ -291,10 +316,16 @@ public function testBackupNonExistantTables() } $this->assertEquals([ - 0 => 'SELECT `table_name` AS `TABLE_NAME` FROM `information_schema`.`tables`' - . ' WHERE `table_schema` = \'' . $db . '\' AND `table_type` = \'BASE TABLE\' AND `table_name` LIKE \'table1\'', - 1 => 'SELECT `table_name` AS `TABLE_NAME` FROM `information_schema`.`tables`' - . ' WHERE `table_schema` = \'' . $db . '\' AND `table_type` = \'BASE TABLE\' AND `table_name` LIKE \'table2\'', + [ + 'sql' => 'SELECT `table_name` AS `TABLE_NAME` FROM `information_schema`.`tables`' + . ' WHERE `table_schema` = ? AND `table_type` = ? AND `table_name` LIKE ?', + 'values' => ['mockedglpi', 'BASE TABLE', 'table1'], + ], + [ + 'sql' => 'SELECT `table_name` AS `TABLE_NAME` FROM `information_schema`.`tables`' + . ' WHERE `table_schema` = ? AND `table_type` = ? AND `table_name` LIKE ?', + 'values' => ['mockedglpi', 'BASE TABLE', 'table2'], + ], ], $migration->getMockedQueries()); } @@ -321,7 +352,7 @@ public function testBackupExistantBackupTables() $this->assertEquals('Unable to rename table glpi_existingtest (ok) to backup_glpi_existingtest (nok)!', $caught->getMessage()); $this->assertEquals([ - 0 => 'DROP TABLE `backup_glpi_existingtest`', + ['sql' => 'DROP TABLE `backup_glpi_existingtest`'], ], $migration->getMockedQueries()); } @@ -338,7 +369,7 @@ public function testBackupTables() $migration->executeMigration(); $this->assertEquals([ - 0 => 'RENAME TABLE `glpi_existingtest` TO `backup_glpi_existingtest`', + ['sql' => 'RENAME TABLE `glpi_existingtest` TO `backup_glpi_existingtest`'], ], $migration->getMockedQueries()); } @@ -351,10 +382,13 @@ public function testChangeField() // Test change field with move to first column $migration->changeField('change_table', 'ID', 'id', 'integer', ['first' => 'first']); $migration->executeMigration(); - $this->assertEquals([ - "ALTER TABLE `change_table` DROP `id` ,\n" - . "CHANGE `ID` `id` INT NOT NULL DEFAULT '0' FIRST ", - ], $migration->getMockedQueries()); + $this->assertEquals( + [ + ['sql' => "ALTER TABLE `change_table` DROP `id` ,\n" + . "CHANGE `ID` `id` INT NOT NULL DEFAULT '0' FIRST "], + ], + $migration->getMockedQueries() + ); // Test change field with move to after another column $migration->clearMockedQueries(); @@ -362,8 +396,8 @@ public function testChangeField() $migration->executeMigration(); $collate = $migration->getMockedDb()->use_utf8mb4 ? 'utf8mb4_unicode_ci' : 'utf8_unicode_ci'; $this->assertEquals([ - "ALTER TABLE `change_table` DROP `name` ,\n" - . "CHANGE `NAME` `name` VARCHAR(255) COLLATE $collate DEFAULT NULL AFTER `id` ", + ['sql' => "ALTER TABLE `change_table` DROP `name` ,\n" + . "CHANGE `NAME` `name` VARCHAR(255) COLLATE $collate DEFAULT NULL AFTER `id` "], ], $migration->getMockedQueries()); } @@ -656,7 +690,15 @@ public function testAddField($table, $field, $format, $options, $sql, array $db_ ]); $migration->addField($table, $field, $format, $options); $migration->executeMigration(); - $this->assertEquals(!is_array($sql) ? [$sql] : $sql, $migration->getMockedQueries()); + if (!is_array($sql)) { + $sql = [$sql]; + } + + $queries = []; + foreach ($sql as $query) { + $queries[] = ['sql' => $query]; + } + $this->assertEquals($queries, $migration->getMockedQueries()); } public function testFormatBooleanBadDefault() @@ -798,7 +840,7 @@ public function testRenameTable() // Case 1, rename with no buffered changes $migration->renameTable('glpi_oldtable', 'glpi_newtable'); $this->assertEquals([ - "RENAME TABLE `glpi_oldtable` TO `glpi_newtable`", + ['sql' => "RENAME TABLE `glpi_oldtable` TO `glpi_newtable`"], ], $migration->getMockedQueries()); // Case 2, rename after changes were already applied @@ -811,12 +853,12 @@ public function testRenameTable() $migration->renameTable('glpi_oldtable', 'glpi_newtable'); $this->assertEquals([ - "SHOW INDEX FROM `glpi_oldtable`", - "SHOW INDEX FROM `glpi_oldtable`", - "ALTER TABLE `glpi_oldtable` ADD `bool_field` TINYINT NOT NULL DEFAULT '0' ", - "ALTER TABLE `glpi_oldtable` ADD FULLTEXT `fulltext_key` (`fulltext_key`)", - "ALTER TABLE `glpi_oldtable` ADD UNIQUE `id` (`id`)", - "RENAME TABLE `glpi_oldtable` TO `glpi_newtable`", + ['sql' => "SHOW INDEX FROM `glpi_oldtable`"], + ['sql' => "SHOW INDEX FROM `glpi_oldtable`"], + ['sql' => "ALTER TABLE `glpi_oldtable` ADD `bool_field` TINYINT NOT NULL DEFAULT '0' "], + ['sql' => "ALTER TABLE `glpi_oldtable` ADD FULLTEXT `fulltext_key` (`fulltext_key`)"], + ['sql' => "ALTER TABLE `glpi_oldtable` ADD UNIQUE `id` (`id`)"], + ['sql' => "RENAME TABLE `glpi_oldtable` TO `glpi_newtable`"], ], $migration->getMockedQueries()); // Case 3, apply changes after renaming @@ -829,12 +871,12 @@ public function testRenameTable() $migration->migrationOneTable('glpi_newtable'); $this->assertEquals([ - "SHOW INDEX FROM `glpi_oldtable`", - "SHOW INDEX FROM `glpi_oldtable`", - "RENAME TABLE `glpi_oldtable` TO `glpi_newtable`", - "ALTER TABLE `glpi_newtable` ADD `bool_field` TINYINT NOT NULL DEFAULT '0' ", - "ALTER TABLE `glpi_newtable` ADD FULLTEXT `fulltext_key` (`fulltext_key`)", - "ALTER TABLE `glpi_newtable` ADD UNIQUE `id` (`id`)", + ['sql' => "SHOW INDEX FROM `glpi_oldtable`"], + ['sql' => "SHOW INDEX FROM `glpi_oldtable`"], + ['sql' => "RENAME TABLE `glpi_oldtable` TO `glpi_newtable`"], + ['sql' => "ALTER TABLE `glpi_newtable` ADD `bool_field` TINYINT NOT NULL DEFAULT '0' "], + ['sql' => "ALTER TABLE `glpi_newtable` ADD FULLTEXT `fulltext_key` (`fulltext_key`)"], + ['sql' => "ALTER TABLE `glpi_newtable` ADD UNIQUE `id` (`id`)"], ], $migration->getMockedQueries()); } @@ -848,7 +890,7 @@ public function testRenameItemtypeWhenSourceTableDoesNotExists() '_mock_tableExists' => false, ]); - $this->expectException(\RuntimeException::class); + $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Table "glpi_someoldtypes" does not exists.'); $migration->renameItemtype('SomeOldType', 'NewName'); } @@ -863,7 +905,7 @@ public function testRenameItemtypeWhenDestinationTableAlreadyExists() '_mock_tableExists' => true, ]); - $this->expectException(\RuntimeException::class); + $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Table "glpi_someoldtypes" cannot be renamed as table "glpi_newnames" already exists.'); $migration->renameItemtype('SomeOldType', 'NewName'); } @@ -886,7 +928,7 @@ public function testRenameItemtypeWhenDestinationFieldAlreadyExists() ]), ]); - $this->expectException(\RuntimeException::class); + $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Field "someoldtypes_id" cannot be renamed in table "glpi_item_with_fkey" as "newnames_id" is field already exists.'); $migration->renameItemtype('SomeOldType', 'NewName'); } @@ -923,10 +965,10 @@ public function testRenameItemtype() ) { // Request used for itemtype fields return new ArrayIterator([ - ['TABLE_NAME' => 'glpi_computers', 'COLUMN_NAME' => 'itemtype'], - ['TABLE_NAME' => 'glpi_users', 'COLUMN_NAME' => 'itemtype'], - ['TABLE_NAME' => 'glpi_stuffs', 'COLUMN_NAME' => 'itemtype_source'], - ['TABLE_NAME' => 'glpi_stuffs', 'COLUMN_NAME' => 'itemtype_dest'], + ['TABLE_NAME' => 'glpi_alerts', 'COLUMN_NAME' => 'itemtype'], + ['TABLE_NAME' => 'glpi_itemantiviruses', 'COLUMN_NAME' => 'itemtype'], + ['TABLE_NAME' => 'glpi_impactrelations', 'COLUMN_NAME' => 'itemtype_source'], + ['TABLE_NAME' => 'glpi_impactrelations', 'COLUMN_NAME' => 'itemtype_impacted'], ]); } return []; @@ -938,14 +980,32 @@ public function testRenameItemtype() $migration->executeMigration(); $this->assertEquals([ - "RENAME TABLE `glpi_someoldtypes` TO `glpi_newnames`", - "UPDATE `glpi_computers` SET `itemtype` = 'NewName' WHERE `itemtype` = 'SomeOldType'", - "UPDATE `glpi_users` SET `itemtype` = 'NewName' WHERE `itemtype` = 'SomeOldType'", - "UPDATE `glpi_stuffs` SET `itemtype_source` = 'NewName' WHERE `itemtype_source` = 'SomeOldType'", - "UPDATE `glpi_stuffs` SET `itemtype_dest` = 'NewName' WHERE `itemtype_dest` = 'SomeOldType'", - "ALTER TABLE `glpi_oneitem_with_fkey` CHANGE `someoldtypes_id` `newnames_id` int unsigned NOT NULL DEFAULT '0' ", - "ALTER TABLE `glpi_anotheritem_with_fkey` CHANGE `someoldtypes_id` `newnames_id` int unsigned NOT NULL DEFAULT '0' ,\n" - . "CHANGE `someoldtypes_id_tech` `newnames_id_tech` int unsigned NOT NULL DEFAULT '0' ", + [ + 'sql' => "RENAME TABLE `glpi_someoldtypes` TO `glpi_newnames`", + ], + [ + 'sql' => "UPDATE `glpi_alerts` SET `itemtype` = ? WHERE `itemtype` = ?", + 'values' => ['NewName', 'SomeOldType'], + ], + [ + 'sql' => "UPDATE `glpi_itemantiviruses` SET `itemtype` = ? WHERE `itemtype` = ?", + 'values' => ['NewName', 'SomeOldType'], + ], + [ + 'sql' => "UPDATE `glpi_impactrelations` SET `itemtype_source` = ? WHERE `itemtype_source` = ?", + 'values' => ['NewName', 'SomeOldType'], + ], + [ + 'sql' => "UPDATE `glpi_impactrelations` SET `itemtype_impacted` = ? WHERE `itemtype_impacted` = ?", + 'values' => ['NewName', 'SomeOldType'], + ], + [ + 'sql' => "ALTER TABLE `glpi_oneitem_with_fkey` CHANGE `someoldtypes_id` `newnames_id` int unsigned NOT NULL DEFAULT '0' ", + ], + [ + 'sql' => "ALTER TABLE `glpi_anotheritem_with_fkey` CHANGE `someoldtypes_id` `newnames_id` int unsigned NOT NULL DEFAULT '0' ,\n" + . "CHANGE `someoldtypes_id_tech` `newnames_id_tech` int unsigned NOT NULL DEFAULT '0' ", + ], ], $migration->getMockedQueries()); // Test renaming without DB structure update @@ -954,10 +1014,22 @@ public function testRenameItemtype() $migration->executeMigration(); $this->assertEquals([ - "UPDATE `glpi_computers` SET `itemtype` = 'NewName' WHERE `itemtype` = 'SomeOldType'", - "UPDATE `glpi_users` SET `itemtype` = 'NewName' WHERE `itemtype` = 'SomeOldType'", - "UPDATE `glpi_stuffs` SET `itemtype_source` = 'NewName' WHERE `itemtype_source` = 'SomeOldType'", - "UPDATE `glpi_stuffs` SET `itemtype_dest` = 'NewName' WHERE `itemtype_dest` = 'SomeOldType'", + [ + 'sql' => "UPDATE `glpi_alerts` SET `itemtype` = ? WHERE `itemtype` = ?", + 'values' => ['NewName', 'SomeOldType'], + ], + [ + 'sql' => "UPDATE `glpi_itemantiviruses` SET `itemtype` = ? WHERE `itemtype` = ?", + 'values' => ['NewName', 'SomeOldType'], + ], + [ + 'sql' => "UPDATE `glpi_impactrelations` SET `itemtype_source` = ? WHERE `itemtype_source` = ?", + 'values' => ['NewName', 'SomeOldType'], + ], + [ + 'sql' => "UPDATE `glpi_impactrelations` SET `itemtype_impacted` = ? WHERE `itemtype_impacted` = ?", + 'values' => ['NewName', 'SomeOldType'], + ], ], $migration->getMockedQueries()); // Test renaming when old class and new class have the same table name @@ -966,10 +1038,22 @@ public function testRenameItemtype() $migration->executeMigration(); $this->assertEquals([ - "UPDATE `glpi_computers` SET `itemtype` = 'GlpiPlugin\\\\Foo\\\\Thing' WHERE `itemtype` = 'PluginFooThing'", - "UPDATE `glpi_users` SET `itemtype` = 'GlpiPlugin\\\\Foo\\\\Thing' WHERE `itemtype` = 'PluginFooThing'", - "UPDATE `glpi_stuffs` SET `itemtype_source` = 'GlpiPlugin\\\\Foo\\\\Thing' WHERE `itemtype_source` = 'PluginFooThing'", - "UPDATE `glpi_stuffs` SET `itemtype_dest` = 'GlpiPlugin\\\\Foo\\\\Thing' WHERE `itemtype_dest` = 'PluginFooThing'", + [ + 'sql' => "UPDATE `glpi_alerts` SET `itemtype` = ? WHERE `itemtype` = ?", + 'values' => ['GlpiPlugin\Foo\Thing', 'PluginFooThing'], + ], + [ + 'sql' => "UPDATE `glpi_itemantiviruses` SET `itemtype` = ? WHERE `itemtype` = ?", + 'values' => ['GlpiPlugin\Foo\Thing', 'PluginFooThing'], + ], + [ + 'sql' => "UPDATE `glpi_impactrelations` SET `itemtype_source` = ? WHERE `itemtype_source` = ?", + 'values' => ['GlpiPlugin\Foo\Thing', 'PluginFooThing'], + ], + [ + 'sql' => "UPDATE `glpi_impactrelations` SET `itemtype_impacted` = ? WHERE `itemtype_impacted` = ?", + 'values' => ['GlpiPlugin\Foo\Thing', 'PluginFooThing'], + ], ], $migration->getMockedQueries()); } @@ -1025,15 +1109,48 @@ public function testChangeSearchOption() $migration->executeMigration(); $this->assertEquals([ - "UPDATE `glpi_displaypreferences` SET `num` = '100' WHERE `itemtype` = 'Computer' AND `num` = '40'", - "DELETE `glpi_displaypreferences` FROM `glpi_displaypreferences` WHERE `id` IN ('12', '156', '421')", - "UPDATE `glpi_displaypreferences` SET `num` = '10' WHERE `itemtype` = 'Printer' AND `num` = '20'", - "UPDATE `glpi_displaypreferences` SET `num` = '1001' WHERE `itemtype` = 'Ticket' AND `num` = '1'", - "UPDATE `glpi_tickettemplatehiddenfields` SET `num` = '1001' WHERE `num` = '1'", - "UPDATE `glpi_tickettemplatemandatoryfields` SET `num` = '1001' WHERE `num` = '1'", - "UPDATE `glpi_tickettemplatepredefinedfields` SET `num` = '1001' WHERE `num` = '1'", - "UPDATE `glpi_savedsearches` SET `query` = 'is_deleted=0&as_map=0&criteria%5B0%5D%5Blink%5D=AND&criteria%5B0%5D%5Bfield%5D=100&criteria%5B0%5D%5Bsearchtype%5D=contains&criteria%5B0%5D%5Bvalue%5D=LT1&criteria%5B1%5D%5Blink%5D=AND&criteria%5B1%5D%5Bitemtype%5D=Budget&criteria%5B1%5D%5Bmeta%5D=1&criteria%5B1%5D%5Bfield%5D=4&criteria%5B1%5D%5Bsearchtype%5D=contains&criteria%5B1%5D%5Bvalue%5D=&search=Search&itemtype=Computer' WHERE `id` = '1'", - "UPDATE `glpi_savedsearches` SET `query` = 'is_deleted=0&as_map=0&criteria%5B0%5D%5Blink%5D=AND&criteria%5B0%5D%5Bfield%5D=40&criteria%5B0%5D%5Bsearchtype%5D=contains&criteria%5B0%5D%5Bvalue%5D=LT1&criteria%5B1%5D%5Blink%5D=AND&criteria%5B1%5D%5Bitemtype%5D=Computer&criteria%5B1%5D%5Bmeta%5D=1&criteria%5B1%5D%5Bfield%5D=100&criteria%5B1%5D%5Bsearchtype%5D=contains&criteria%5B1%5D%5Bvalue%5D=&search=Search&itemtype=Computer' WHERE `id` = '2'", + [ + 'sql' => "UPDATE `glpi_displaypreferences` SET `num` = ? WHERE `itemtype` = ? AND `num` = ?", + 'values' => [100, 'Computer', 40], + ], + [ + 'sql' => "DELETE `glpi_displaypreferences` FROM `glpi_displaypreferences` WHERE `id` IN (?, ?, ?)", + 'values' => [12, 156, 421], + ], + [ + 'sql' => "UPDATE `glpi_displaypreferences` SET `num` = ? WHERE `itemtype` = ? AND `num` = ?", + 'values' => [10, 'Printer', 20], + ], + [ + 'sql' => "UPDATE `glpi_displaypreferences` SET `num` = ? WHERE `itemtype` = ? AND `num` = ?", + 'values' => [1001, 'Ticket', 1], + ], + [ + 'sql' => "UPDATE `glpi_tickettemplatehiddenfields` SET `num` = ? WHERE `num` = ?", + 'values' => [1001, 1], + ], + [ + 'sql' => "UPDATE `glpi_tickettemplatemandatoryfields` SET `num` = ? WHERE `num` = ?", + 'values' => [1001, 1], + ], + [ + 'sql' => "UPDATE `glpi_tickettemplatepredefinedfields` SET `num` = ? WHERE `num` = ?", + 'values' => [1001, 1], + ], + [ + 'sql' => "UPDATE `glpi_savedsearches` SET `query` = ? WHERE `id` = ?", + 'values' => [ + 'is_deleted=0&as_map=0&criteria%5B0%5D%5Blink%5D=AND&criteria%5B0%5D%5Bfield%5D=100&criteria%5B0%5D%5Bsearchtype%5D=contains&criteria%5B0%5D%5Bvalue%5D=LT1&criteria%5B1%5D%5Blink%5D=AND&criteria%5B1%5D%5Bitemtype%5D=Budget&criteria%5B1%5D%5Bmeta%5D=1&criteria%5B1%5D%5Bfield%5D=4&criteria%5B1%5D%5Bsearchtype%5D=contains&criteria%5B1%5D%5Bvalue%5D=&search=Search&itemtype=Computer', + 1, + ], + ], + [ + 'sql' => "UPDATE `glpi_savedsearches` SET `query` = ? WHERE `id` = ?", + 'values' => [ + 'is_deleted=0&as_map=0&criteria%5B0%5D%5Blink%5D=AND&criteria%5B0%5D%5Bfield%5D=40&criteria%5B0%5D%5Bsearchtype%5D=contains&criteria%5B0%5D%5Bvalue%5D=LT1&criteria%5B1%5D%5Blink%5D=AND&criteria%5B1%5D%5Bitemtype%5D=Computer&criteria%5B1%5D%5Bmeta%5D=1&criteria%5B1%5D%5Bfield%5D=100&criteria%5B1%5D%5Bsearchtype%5D=contains&criteria%5B1%5D%5Bvalue%5D=&search=Search&itemtype=Computer', + 2, + ], + ], ], $migration->getMockedQueries()); } @@ -1089,14 +1206,44 @@ public function testRemoveSearchOption() $migration->executeMigration(); $this->assertEquals([ - "DELETE `glpi_displaypreferences` FROM `glpi_displaypreferences` WHERE `itemtype` = 'Computer' AND `num` = '40'", - "DELETE `glpi_displaypreferences` FROM `glpi_displaypreferences` WHERE `itemtype` = 'Printer' AND `num` = '20'", - "DELETE `glpi_displaypreferences` FROM `glpi_displaypreferences` WHERE `itemtype` = 'Ticket' AND `num` = '1'", - "DELETE `glpi_tickettemplatehiddenfields` FROM `glpi_tickettemplatehiddenfields` WHERE `num` = '1'", - "DELETE `glpi_tickettemplatemandatoryfields` FROM `glpi_tickettemplatemandatoryfields` WHERE `num` = '1'", - "DELETE `glpi_tickettemplatepredefinedfields` FROM `glpi_tickettemplatepredefinedfields` WHERE `num` = '1'", - "UPDATE `glpi_savedsearches` SET `query` = 'is_deleted=0&as_map=0&criteria%5B1%5D%5Blink%5D=AND&criteria%5B1%5D%5Bitemtype%5D=Budget&criteria%5B1%5D%5Bmeta%5D=1&criteria%5B1%5D%5Bfield%5D=4&criteria%5B1%5D%5Bsearchtype%5D=contains&criteria%5B1%5D%5Bvalue%5D=&search=Search&itemtype=Computer' WHERE `id` = '1'", - "UPDATE `glpi_savedsearches` SET `query` = 'is_deleted=0&as_map=0&criteria%5B0%5D%5Blink%5D=AND&criteria%5B0%5D%5Bfield%5D=40&criteria%5B0%5D%5Bsearchtype%5D=contains&criteria%5B0%5D%5Bvalue%5D=LT1&search=Search&itemtype=Computer' WHERE `id` = '2'", + [ + 'sql' => "DELETE `glpi_displaypreferences` FROM `glpi_displaypreferences` WHERE `itemtype` = ? AND `num` = ?", + 'values' => ['Computer', 40], + ], + [ + 'sql' => "DELETE `glpi_displaypreferences` FROM `glpi_displaypreferences` WHERE `itemtype` = ? AND `num` = ?", + 'values' => ['Printer', 20], + ], + [ + 'sql' => "DELETE `glpi_displaypreferences` FROM `glpi_displaypreferences` WHERE `itemtype` = ? AND `num` = ?", + 'values' => ['Ticket', 1], + ], + [ + 'sql' => "DELETE `glpi_tickettemplatehiddenfields` FROM `glpi_tickettemplatehiddenfields` WHERE `num` = ?", + 'values' => [1], + ], + [ + 'sql' => "DELETE `glpi_tickettemplatemandatoryfields` FROM `glpi_tickettemplatemandatoryfields` WHERE `num` = ?", + 'values' => [1], + ], + [ + 'sql' => "DELETE `glpi_tickettemplatepredefinedfields` FROM `glpi_tickettemplatepredefinedfields` WHERE `num` = ?", + 'values' => [1], + ], + [ + 'sql' => "UPDATE `glpi_savedsearches` SET `query` = ? WHERE `id` = ?", + 'values' => [ + 'is_deleted=0&as_map=0&criteria%5B1%5D%5Blink%5D=AND&criteria%5B1%5D%5Bitemtype%5D=Budget&criteria%5B1%5D%5Bmeta%5D=1&criteria%5B1%5D%5Bfield%5D=4&criteria%5B1%5D%5Bsearchtype%5D=contains&criteria%5B1%5D%5Bvalue%5D=&search=Search&itemtype=Computer', + 1, + ], + ], + [ + 'sql' => "UPDATE `glpi_savedsearches` SET `query` = ? WHERE `id` = ?", + 'values' => [ + 'is_deleted=0&as_map=0&criteria%5B0%5D%5Blink%5D=AND&criteria%5B0%5D%5Bfield%5D=40&criteria%5B0%5D%5Bsearchtype%5D=contains&criteria%5B0%5D%5Bvalue%5D=LT1&search=Search&itemtype=Computer', + 2, + ], + ], ], $migration->getMockedQueries()); } @@ -1349,7 +1496,7 @@ public function __construct(array $options) public function doQuery($query) { - $this->_queries[] = $query; + $this->_queries[] = ['sql' => $query]; if (isset($this->_mock_options['_mock_doQuery'])) { return is_callable($this->_mock_options['_mock_doQuery']) ? ($this->_mock_options['_mock_doQuery'])($query) @@ -1419,6 +1566,43 @@ public function quote($value, int $type = 2) global $DB; return $DB->quote($value, $type); } + + public function prepare($query) + { + // Proxy prepare to the real DB instance since the mock has no mysqli handler + global $DB; + $res = $DB->prepare($query); + if (!$res) { + throw new RuntimeException( + sprintf( + 'MySQL prepare error: %s (%d) in SQL query "%s".', + $this->dbh->error, + $this->dbh->errno, + $query + ) + ); + } + $property = new ReflectionProperty(DBmysql::class, 'current_query'); + $property->setValue($this, $query); + return $res; + } + + public function executeStatement(mysqli_stmt $stmt, ?array $params = null, ?array $types = null): void + { + $params = array_values($params); //no need for the keys + foreach ($params as &$param) { + if ($param === false) { + $param = 0; + } + } + $property = new ReflectionProperty(DBmysql::class, 'current_query'); + $this->_queries[] = [ + 'sql' => $property->getValue($this), + 'values' => $params, + ]; + + parent::executeStatement($stmt, $params, $types); + } }; $db->disableTableCaching(); return $db; @@ -1429,7 +1613,7 @@ private function getMigrationMock($ver = GLPI_VERSION, ?AbstractProgressIndicato global $DB; $db = ($db_options['_real_db'] ?? false) ? $DB : $this->getDbMock($db_options); foreach ($db_options as $property => $value) { - if (property_exists(\DBmysql::class, $property)) { + if (property_exists(DBmysql::class, $property)) { $db->{$property} = $value; } } @@ -1466,7 +1650,7 @@ public function clearMockedQueries(): void $this->db->_queries = []; } - public function getMockedDb(): \DBmysql + public function getMockedDb(): DBmysql { return $this->db; } diff --git a/tests/src/Command/TestUpdatedDataCommand.php b/tests/src/Command/TestUpdatedDataCommand.php index 11a3ff0d10a..0189c6ec69b 100644 --- a/tests/src/Command/TestUpdatedDataCommand.php +++ b/tests/src/Command/TestUpdatedDataCommand.php @@ -44,6 +44,8 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\Question; +use function Safe\json_encode; + class TestUpdatedDataCommand extends Command { protected function configure() @@ -263,6 +265,12 @@ private function hasMissingRowsInUpdatedDb(DBmysql $fresh_db, DBmysql $updated_d if ($found_in_updated->count() !== 1) { $missing = true; $msg = sprintf('Unable to find the following object in table "%s": %s', $table_name, json_encode($row_data)); + $msg .= sprintf( + "Query:\n%s\nParameters:\n%s\n%s", + $found_in_updated->getSql(), + json_encode($found_in_updated->getValues()), + var_export($criteria, true) + ); $output->writeln(' ' . $msg, OutputInterface::VERBOSITY_QUIET); } }