*/ private array $conditionArray = []; /** * Function to generate unique condition for specified row. * * @param FieldMetadata[] $fieldsMeta meta information about fields * @param array $row current row * @param bool $forceUnique generate condition only on pk or unique * @param string $restrictToTable restrict the unique condition to this table * @param (Expression|CaseExpression)[] $expressions An array of Expression instances. * @psalm-param array $row */ public function __construct( array $fieldsMeta, array $row, bool $forceUnique = false, string $restrictToTable = '', array $expressions = [], ) { $fieldsCount = count($fieldsMeta); $primaryKey = ''; $uniqueKey = ''; $nonPrimaryCondition = ''; $preferredCondition = ''; $primaryKeyArray = []; $uniqueKeyArray = []; $nonPrimaryConditionArray = []; foreach ($fieldsMeta as $i => $meta) { // do not use a column alias in a condition if ($meta->orgname === '') { $meta->orgname = $meta->name; foreach ($expressions as $expression) { if ( $expression->alias === null || $expression->alias === '' || $expression instanceof CaseExpression || $expression->column === null || $expression->column === '' ) { continue; } if (strcasecmp($meta->name, $expression->alias) == 0) { $meta->orgname = $expression->column; break; } } } // Do not use a table alias in a condition. // Test case is: // select * from galerie x WHERE //(select count(*) from galerie y where y.datum=x.datum)>1 // // Also, do not use the original table name if we are dealing with // a view because this view might be updatable. // (The isView() verification should not be costly in most cases // because there is some caching in the function). if ( $meta->table !== $meta->orgtable && ! DatabaseInterface::getInstance()->getTable(Current::$database, $meta->table)->isView() ) { $meta->table = $meta->orgtable; } // If this field is not from the table which the unique clause needs // to be restricted to. if ($restrictToTable !== '' && $restrictToTable != $meta->table) { continue; } // to fix the bug where float fields (primary or not) // can't be matched because of the imprecision of // floating comparison, use CONCAT // (also, the syntax "CONCAT(field) IS NULL" // that we need on the next "if" will work) if ($meta->isType(FieldMetadata::TYPE_REAL)) { $conKey = 'CONCAT(' . Util::backquote($meta->table) . '.' . Util::backquote($meta->orgname) . ')'; } else { $conKey = Util::backquote($meta->table) . '.' . Util::backquote($meta->orgname); } $condition = ' ' . $conKey . ' '; [$conVal, $condition] = $this->getConditionValue( $row[$i] ?? null, $meta, $fieldsCount, $conKey, $condition, ); if ($conVal === null) { continue; } $condition .= $conVal . ' AND'; if ($meta->isPrimaryKey()) { $primaryKey .= $condition; $primaryKeyArray[$conKey] = $conVal; } elseif ($meta->isUniqueKey()) { $uniqueKey .= $condition; $uniqueKeyArray[$conKey] = $conVal; } $nonPrimaryCondition .= $condition; $nonPrimaryConditionArray[$conKey] = $conVal; } // Correction University of Virginia 19991216: // prefer primary or unique keys for condition, // but use conjunction of all values if no primary key if ($primaryKey !== '') { $preferredCondition = $primaryKey; $this->conditionArray = $primaryKeyArray; } elseif ($uniqueKey !== '') { $preferredCondition = $uniqueKey; $this->conditionArray = $uniqueKeyArray; } elseif (! $forceUnique) { $preferredCondition = $nonPrimaryCondition; $this->conditionArray = $nonPrimaryConditionArray; $this->clauseIsUnique = false; } $this->whereClause = trim((string) preg_replace('|\s?AND$|', '', $preferredCondition)); } public function getWhereClause(): string { return $this->whereClause; } public function isClauseUnique(): bool { return $this->clauseIsUnique; } /** @return array */ public function getConditionArray(): array { return $this->conditionArray; } /** * Build a condition and with a value * * @param string|int|float|null $row The row value * @param FieldMetadata $meta The field metadata * @param int $fieldsCount A number of fields * @param string $conditionKey A key used for BINARY fields functions * @param string $condition The condition * * @return array * @psalm-return array{string|null, string} */ private function getConditionValue( string|int|float|null $row, FieldMetadata $meta, int $fieldsCount, string $conditionKey, string $condition, ): array { if ($row === null) { return ['IS NULL', $condition]; } $conditionValue = ''; $isBinaryString = $meta->isType(FieldMetadata::TYPE_STRING) && $meta->isBinary(); // 63 is the binary charset, see: https://dev.mysql.com/doc/internals/en/charsets.html $isBlobAndIsBinaryCharset = $meta->isType(FieldMetadata::TYPE_BLOB) && $meta->charsetnr === 63; if ($meta->isNumeric) { $conditionValue = '= ' . $row; } elseif ($isBlobAndIsBinaryCharset || ($row != 0 && $isBinaryString)) { // hexify only if this is a true not empty BLOB or a BINARY // do not waste memory building a too big condition $rowLength = mb_strlen((string) $row); if ($rowLength > 0 && $rowLength < 1000) { // use a CAST if possible, to avoid problems // if the field contains wildcard characters % or _ $conditionValue = '= CAST(0x' . bin2hex((string) $row) . ' AS BINARY)'; } elseif ($fieldsCount === 1) { // when this blob is the only field present // try settling with length comparison $condition = ' CHAR_LENGTH(' . $conditionKey . ') '; $conditionValue = ' = ' . $rowLength; } else { // this blob won't be part of the final condition $conditionValue = null; } } elseif ($meta->isMappedTypeGeometry && $row != 0) { // do not build a too big condition if (mb_strlen((string) $row) < 5000) { $condition .= '= CAST(0x' . bin2hex((string) $row) . ' AS BINARY)'; } else { $condition = ''; } } elseif ($meta->isMappedTypeBit) { $conditionValue = "= b'" . Util::printableBitValue((int) $row, $meta->length) . "'"; } else { $conditionValue = '= ' . DatabaseInterface::getInstance()->quoteString((string) $row); } return [$conditionValue, $condition]; } }