From f84f3c71850a1f58b520d3444f2ff7b122a148c9 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Tue, 3 Feb 2026 16:02:40 +0100 Subject: [PATCH] Begin work on prepared statements Rework affected rows (must be retrieved from statement) Handle Prepared from migrations Handle possible exisiting QueryExpression statement values Try to detect early bind issues Limit to simple cases for now Fix prepared IN, fix buildUpdate Try to get extra information on failure rawAnalyzeCrit (maybe to remove) --- install/migrations/update_9.4.3_to_9.4.5.php | 4 +- src/AbstractQuery.php | 19 + src/Cartridge.php | 6 +- src/CartridgeItem.php | 2 +- src/CommonDBTM.php | 17 +- src/CronTask.php | 4 +- src/CronTaskLog.php | 2 +- src/DBmysql.php | 152 +++++--- src/DBmysqlIterator.php | 102 +++++- src/Glpi/DBAL/Delete.php | 37 ++ src/Glpi/DBAL/Insert.php | 37 ++ src/Glpi/DBAL/JsonFieldInterface.php | 1 - src/Glpi/DBAL/Prepared.php | 75 ++++ src/Glpi/DBAL/QueryExpression.php | 21 +- src/Glpi/DBAL/QueryFunction.php | 143 +++++++- src/Glpi/DBAL/QueryParam.php | 1 - src/Glpi/DBAL/QuerySubQuery.php | 9 +- src/Glpi/DBAL/QueryUnion.php | 10 +- src/Glpi/DBAL/Update.php | 37 ++ src/Glpi/Dashboard/Provider.php | 14 +- src/Glpi/Event.php | 2 +- .../Inventory/Asset/InventoryNetworkPort.php | 2 +- src/Log.php | 2 +- src/Migration.php | 38 +- src/Profile.php | 3 +- src/ProfileRight.php | 5 +- src/QueuedNotification.php | 4 +- src/QueuedWebhook.php | 2 +- tests/functional/CommonDBTMTest.php | 2 +- tests/functional/CronTaskTest.php | 2 +- tests/functional/DBTest.php | 58 ++- tests/functional/DBmysqlIteratorTest.php | 249 ++++++++----- tests/functional/MigrationTest.php | 342 ++++++++++++++---- tests/src/Command/TestUpdatedDataCommand.php | 8 + 34 files changed, 1119 insertions(+), 293 deletions(-) create mode 100644 src/Glpi/DBAL/Delete.php create mode 100644 src/Glpi/DBAL/Insert.php create mode 100644 src/Glpi/DBAL/Prepared.php create mode 100644 src/Glpi/DBAL/Update.php 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); } }