Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 81 additions & 78 deletions src/Database/Adapter/MariaDB.php
Original file line number Diff line number Diff line change
Expand Up @@ -1037,7 +1037,7 @@ public function updateDocument(Document $collection, string $id, Document $docum
* Update Attributes
*/
$keyIndex = 0;
$opIndex = 0;
$operatorBinds = [];
$operators = [];

// Separate regular attributes from operators
Expand All @@ -1052,7 +1052,7 @@ public function updateDocument(Document $collection, string $id, Document $docum

// Check if this is an operator or regular attribute
if (isset($operators[$attribute])) {
$operatorSQL = $this->getOperatorSQL($column, $operators[$attribute], $opIndex);
$operatorSQL = $this->getOperatorSQL($column, $operators[$attribute], $operatorBinds);
$columns .= $operatorSQL . ',';
} else {
$bindKey = 'key_' . $keyIndex;
Expand Down Expand Up @@ -1084,26 +1084,29 @@ public function updateDocument(Document $collection, string $id, Document $docum
}

$keyIndex = 0;
$opIndexForBinding = 0;
foreach ($attributes as $attribute => $value) {
// Handle operators separately
if (isset($operators[$attribute])) {
$this->bindOperatorParams($stmt, $operators[$attribute], $opIndexForBinding);
} else {
// Convert spatial arrays to WKT, json_encode non-spatial arrays
if (\in_array($attribute, $spatialAttributes, true)) {
if (\is_array($value)) {
$value = $this->convertArrayToWKT($value);
}
} elseif (is_array($value)) {
$value = json_encode($value);
}
continue;
}

$bindKey = 'key_' . $keyIndex;
$value = (is_bool($value)) ? (int)$value : $value;
$stmt->bindValue(':' . $bindKey, $value, $this->getPDOType($value));
$keyIndex++;
// Convert spatial arrays to WKT, json_encode non-spatial arrays
if (\in_array($attribute, $spatialAttributes, true)) {
if (\is_array($value)) {
$value = $this->convertArrayToWKT($value);
}
} elseif (is_array($value)) {
$value = json_encode($value);
}

$bindKey = 'key_' . $keyIndex;
$value = (is_bool($value)) ? (int)$value : $value;
$stmt->bindValue(':' . $bindKey, $value, $this->getPDOType($value));
$keyIndex++;
}

foreach ($operatorBinds as $bindKey => $bindValue) {
$stmt->bindValue($bindKey, $bindValue, $this->getPDOType($bindValue));
}

$stmt->execute();
Expand Down Expand Up @@ -1159,7 +1162,7 @@ public function getUpsertStatement(
};

$updateColumns = [];
$opIndex = 0;
$operatorBinds = [];

if (!empty($attribute)) {
// Increment specific column by its new value in place
Expand All @@ -1175,7 +1178,7 @@ public function getUpsertStatement(
$filteredAttr = $this->filter($attr);

if (isset($operators[$attr])) {
$operatorSQL = $this->getOperatorSQL($filteredAttr, $operators[$attr], $opIndex);
$operatorSQL = $this->getOperatorSQL($filteredAttr, $operators[$attr], $operatorBinds);
if ($operatorSQL !== null) {
$updateColumns[] = $operatorSQL;
}
Expand All @@ -1199,11 +1202,8 @@ public function getUpsertStatement(
$stmt->bindValue($key, $binding, $this->getPDOType($binding));
}

$opIndexForBinding = 0;
foreach (\array_keys($attributes) as $attr) {
if (isset($operators[$attr])) {
$this->bindOperatorParams($stmt, $operators[$attr], $opIndexForBinding);
}
foreach ($operatorBinds as $bindKey => $bindValue) {
$stmt->bindValue($bindKey, $bindValue, $this->getPDOType($bindValue));
}

return $stmt;
Expand Down Expand Up @@ -1951,10 +1951,10 @@ protected function quote(string $string): string
*
* @param string $column
* @param Operator $operator
* @param int &$bindIndex
* @param array<string, mixed> $binds
* @return ?string
*/
protected function getOperatorSQL(string $column, Operator $operator, int &$bindIndex): ?string
protected function getOperatorSQL(string $column, Operator $operator, array &$binds): ?string
{
$quotedColumn = $this->quote($column);
$method = $operator->getMethod();
Expand All @@ -1963,11 +1963,9 @@ protected function getOperatorSQL(string $column, Operator $operator, int &$bind
switch ($method) {
// Numeric operators
case Operator::TYPE_INCREMENT:
$bindKey = "op_{$bindIndex}";
$bindIndex++;
$bindKey = $this->registerOperatorBind($binds, $values[0] ?? 1);
if (isset($values[1])) {
$maxKey = "op_{$bindIndex}";
$bindIndex++;
$maxKey = $this->registerOperatorBind($binds, $values[1]);
return "{$quotedColumn} = CASE
WHEN COALESCE({$quotedColumn}, 0) >= :$maxKey THEN :$maxKey
WHEN COALESCE({$quotedColumn}, 0) > :$maxKey - :$bindKey THEN :$maxKey
Expand All @@ -1977,11 +1975,9 @@ protected function getOperatorSQL(string $column, Operator $operator, int &$bind
return "{$quotedColumn} = COALESCE({$quotedColumn}, 0) + :$bindKey";

case Operator::TYPE_DECREMENT:
$bindKey = "op_{$bindIndex}";
$bindIndex++;
$bindKey = $this->registerOperatorBind($binds, $values[0] ?? 1);
if (isset($values[1])) {
$minKey = "op_{$bindIndex}";
$bindIndex++;
$minKey = $this->registerOperatorBind($binds, $values[1]);
return "{$quotedColumn} = CASE
WHEN COALESCE({$quotedColumn}, 0) <= :$minKey THEN :$minKey
WHEN COALESCE({$quotedColumn}, 0) < :$minKey + :$bindKey THEN :$minKey
Expand All @@ -1991,11 +1987,9 @@ protected function getOperatorSQL(string $column, Operator $operator, int &$bind
return "{$quotedColumn} = COALESCE({$quotedColumn}, 0) - :$bindKey";

case Operator::TYPE_MULTIPLY:
$bindKey = "op_{$bindIndex}";
$bindIndex++;
$bindKey = $this->registerOperatorBind($binds, $values[0] ?? 1);
if (isset($values[1])) {
$maxKey = "op_{$bindIndex}";
$bindIndex++;
$maxKey = $this->registerOperatorBind($binds, $values[1]);
return "{$quotedColumn} = CASE
WHEN COALESCE({$quotedColumn}, 0) >= :$maxKey THEN :$maxKey
WHEN :$bindKey > 0 AND COALESCE({$quotedColumn}, 0) > :$maxKey / :$bindKey THEN :$maxKey
Expand All @@ -2006,11 +2000,9 @@ protected function getOperatorSQL(string $column, Operator $operator, int &$bind
return "{$quotedColumn} = COALESCE({$quotedColumn}, 0) * :$bindKey";

case Operator::TYPE_DIVIDE:
$bindKey = "op_{$bindIndex}";
$bindIndex++;
$bindKey = $this->registerOperatorBind($binds, $values[0] ?? 1);
if (isset($values[1])) {
$minKey = "op_{$bindIndex}";
$bindIndex++;
$minKey = $this->registerOperatorBind($binds, $values[1]);
return "{$quotedColumn} = CASE
WHEN :$bindKey != 0 AND COALESCE({$quotedColumn}, 0) / :$bindKey <= :$minKey THEN :$minKey
ELSE COALESCE({$quotedColumn}, 0) / :$bindKey
Expand All @@ -2019,16 +2011,13 @@ protected function getOperatorSQL(string $column, Operator $operator, int &$bind
return "{$quotedColumn} = COALESCE({$quotedColumn}, 0) / :$bindKey";

case Operator::TYPE_MODULO:
$bindKey = "op_{$bindIndex}";
$bindIndex++;
$bindKey = $this->registerOperatorBind($binds, $values[0] ?? 1);
return "{$quotedColumn} = MOD(COALESCE({$quotedColumn}, 0), :$bindKey)";

case Operator::TYPE_POWER:
$bindKey = "op_{$bindIndex}";
$bindIndex++;
$bindKey = $this->registerOperatorBind($binds, $values[0] ?? 1);
if (isset($values[1])) {
$maxKey = "op_{$bindIndex}";
$bindIndex++;
$maxKey = $this->registerOperatorBind($binds, $values[1]);
return "{$quotedColumn} = CASE
WHEN COALESCE({$quotedColumn}, 0) >= :$maxKey THEN :$maxKey
WHEN COALESCE({$quotedColumn}, 0) <= 1 THEN COALESCE({$quotedColumn}, 0)
Expand All @@ -2040,15 +2029,12 @@ protected function getOperatorSQL(string $column, Operator $operator, int &$bind

// String operators
case Operator::TYPE_STRING_CONCAT:
$bindKey = "op_{$bindIndex}";
$bindIndex++;
$bindKey = $this->registerOperatorBind($binds, $values[0] ?? '');
return "{$quotedColumn} = CONCAT(COALESCE({$quotedColumn}, ''), :$bindKey)";

case Operator::TYPE_STRING_REPLACE:
$searchKey = "op_{$bindIndex}";
$bindIndex++;
$replaceKey = "op_{$bindIndex}";
$bindIndex++;
$searchKey = $this->registerOperatorBind($binds, $values[0] ?? '');
$replaceKey = $this->registerOperatorBind($binds, $values[1] ?? '');
return "{$quotedColumn} = REPLACE({$quotedColumn}, :$searchKey, :$replaceKey)";

// Boolean operators
Expand All @@ -2057,29 +2043,36 @@ protected function getOperatorSQL(string $column, Operator $operator, int &$bind

// Array operators
case Operator::TYPE_ARRAY_APPEND:
$bindKey = "op_{$bindIndex}";
$bindIndex++;
if (\count($values) > self::MAX_ARRAY_OPERATOR_SIZE) {
throw new DatabaseException("Array size " . \count($values) . " exceeds maximum allowed size of " . self::MAX_ARRAY_OPERATOR_SIZE . " for array operations");
}
$bindKey = $this->registerOperatorBind($binds, json_encode($values));
return "{$quotedColumn} = JSON_MERGE_PRESERVE(IFNULL({$quotedColumn}, JSON_ARRAY()), :$bindKey)";

case Operator::TYPE_ARRAY_PREPEND:
$bindKey = "op_{$bindIndex}";
$bindIndex++;
if (\count($values) > self::MAX_ARRAY_OPERATOR_SIZE) {
throw new DatabaseException("Array size " . \count($values) . " exceeds maximum allowed size of " . self::MAX_ARRAY_OPERATOR_SIZE . " for array operations");
}
$bindKey = $this->registerOperatorBind($binds, json_encode($values));
return "{$quotedColumn} = JSON_MERGE_PRESERVE(:$bindKey, IFNULL({$quotedColumn}, JSON_ARRAY()))";

case Operator::TYPE_ARRAY_INSERT:
$indexKey = "op_{$bindIndex}";
$bindIndex++;
$valueKey = "op_{$bindIndex}";
$bindIndex++;
$indexKey = $this->registerOperatorBind($binds, $values[0] ?? 0);
$valueKey = $this->registerOperatorBind($binds, json_encode($values[1] ?? null));
return "{$quotedColumn} = JSON_ARRAY_INSERT(
{$quotedColumn},
CONCAT('$[', :$indexKey, ']'),
{$quotedColumn},
CONCAT('$[', :$indexKey, ']'),
JSON_EXTRACT(:$valueKey, '$')
)";

case Operator::TYPE_ARRAY_REMOVE:
$bindKey = "op_{$bindIndex}";
$bindIndex++;
$removeValue = $values[0] ?? null;
// Cast scalars to string so the value binds as PDO::PARAM_STR, preserving the
// pre-refactor behavior (it was bound with an explicit PARAM_STR). JSON_TABLE
// extracts `value` as TEXT, so the search term must compare as text — without
// the cast, getPDOType() would bind a number as PARAM_INT. Do not drop it.
$removeValue = is_array($removeValue) ? json_encode($removeValue) : (string)$removeValue;
$bindKey = $this->registerOperatorBind($binds, $removeValue);
Comment thread
fogelito marked this conversation as resolved.
return "{$quotedColumn} = IFNULL((
SELECT JSON_ARRAYAGG(value)
FROM JSON_TABLE({$quotedColumn}, '\$[*]' COLUMNS(value TEXT PATH '\$')) AS jt
Expand All @@ -2093,8 +2086,10 @@ protected function getOperatorSQL(string $column, Operator $operator, int &$bind
), JSON_ARRAY())";

case Operator::TYPE_ARRAY_INTERSECT:
$bindKey = "op_{$bindIndex}";
$bindIndex++;
if (\count($values) > self::MAX_ARRAY_OPERATOR_SIZE) {
throw new DatabaseException("Array size " . \count($values) . " exceeds maximum allowed size of " . self::MAX_ARRAY_OPERATOR_SIZE . " for array operations");
}
$bindKey = $this->registerOperatorBind($binds, json_encode($values));
return "{$quotedColumn} = IFNULL((
SELECT JSON_ARRAYAGG(jt1.value)
FROM JSON_TABLE({$quotedColumn}, '\$[*]' COLUMNS(value TEXT PATH '\$')) AS jt1
Expand All @@ -2105,8 +2100,10 @@ protected function getOperatorSQL(string $column, Operator $operator, int &$bind
), JSON_ARRAY())";

case Operator::TYPE_ARRAY_DIFF:
$bindKey = "op_{$bindIndex}";
$bindIndex++;
if (\count($values) > self::MAX_ARRAY_OPERATOR_SIZE) {
throw new DatabaseException("Array size " . \count($values) . " exceeds maximum allowed size of " . self::MAX_ARRAY_OPERATOR_SIZE . " for array operations");
}
$bindKey = $this->registerOperatorBind($binds, json_encode($values));
return "{$quotedColumn} = IFNULL((
SELECT JSON_ARRAYAGG(jt1.value)
FROM JSON_TABLE({$quotedColumn}, '\$[*]' COLUMNS(value TEXT PATH '\$')) AS jt1
Expand All @@ -2117,10 +2114,18 @@ protected function getOperatorSQL(string $column, Operator $operator, int &$bind
), JSON_ARRAY())";

case Operator::TYPE_ARRAY_FILTER:
$conditionKey = "op_{$bindIndex}";
$bindIndex++;
$valueKey = "op_{$bindIndex}";
$bindIndex++;
$condition = $values[0] ?? 'equal';
$validConditions = [
'equal', 'notEqual', // Comparison
'greaterThan', 'greaterThanEqual', 'lessThan', 'lessThanEqual', // Numeric
'isNull', 'isNotNull' // Null checks
];
if (!in_array($condition, $validConditions, true)) {
throw new DatabaseException("Invalid filter condition: {$condition}. Must be one of: " . implode(', ', $validConditions));
}
$filterValue = $values[1] ?? null;
$conditionKey = $this->registerOperatorBind($binds, $condition);
$valueKey = $this->registerOperatorBind($binds, $filterValue === null ? null : json_encode($filterValue));
return "{$quotedColumn} = IFNULL((
SELECT JSON_ARRAYAGG(value)
FROM JSON_TABLE({$quotedColumn}, '\$[*]' COLUMNS(value TEXT PATH '\$')) AS jt
Expand All @@ -2139,13 +2144,11 @@ protected function getOperatorSQL(string $column, Operator $operator, int &$bind

// Date operators
case Operator::TYPE_DATE_ADD_DAYS:
$bindKey = "op_{$bindIndex}";
$bindIndex++;
$bindKey = $this->registerOperatorBind($binds, $values[0] ?? 0);
return "{$quotedColumn} = DATE_ADD({$quotedColumn}, INTERVAL :$bindKey DAY)";

case Operator::TYPE_DATE_SUB_DAYS:
$bindKey = "op_{$bindIndex}";
$bindIndex++;
$bindKey = $this->registerOperatorBind($binds, $values[0] ?? 0);
return "{$quotedColumn} = DATE_SUB({$quotedColumn}, INTERVAL :$bindKey DAY)";

case Operator::TYPE_DATE_SET_NOW:
Expand Down
19 changes: 12 additions & 7 deletions src/Database/Adapter/MySQL.php
Original file line number Diff line number Diff line change
Expand Up @@ -287,23 +287,28 @@ public function getSupportForOptionalSpatialAttributeWithExistingRows(): bool
*
* @param string $column
* @param \Utopia\Database\Operator $operator
* @param int &$bindIndex
* @param array<string, mixed> $binds
* @return ?string
*/
protected function getOperatorSQL(string $column, \Utopia\Database\Operator $operator, int &$bindIndex): ?string
protected function getOperatorSQL(string $column, \Utopia\Database\Operator $operator, array &$binds): ?string
{
$quotedColumn = $this->quote($column);
$method = $operator->getMethod();
$values = $operator->getValues();

switch ($method) {
case Operator::TYPE_ARRAY_APPEND:
$bindKey = "op_{$bindIndex}";
$bindIndex++;
if (\count($values) > self::MAX_ARRAY_OPERATOR_SIZE) {
throw new DatabaseException("Array size " . \count($values) . " exceeds maximum allowed size of " . self::MAX_ARRAY_OPERATOR_SIZE . " for array operations");
}
$bindKey = $this->registerOperatorBind($binds, json_encode($values));
return "{$quotedColumn} = JSON_MERGE_PRESERVE(IFNULL({$quotedColumn}, JSON_ARRAY()), :$bindKey)";

case Operator::TYPE_ARRAY_PREPEND:
$bindKey = "op_{$bindIndex}";
$bindIndex++;
if (\count($values) > self::MAX_ARRAY_OPERATOR_SIZE) {
throw new DatabaseException("Array size " . \count($values) . " exceeds maximum allowed size of " . self::MAX_ARRAY_OPERATOR_SIZE . " for array operations");
}
$bindKey = $this->registerOperatorBind($binds, json_encode($values));
return "{$quotedColumn} = JSON_MERGE_PRESERVE(:$bindKey, IFNULL({$quotedColumn}, JSON_ARRAY()))";

case Operator::TYPE_ARRAY_UNIQUE:
Expand All @@ -317,7 +322,7 @@ protected function getOperatorSQL(string $column, \Utopia\Database\Operator $ope
}

// For all other operators, use parent implementation
return parent::getOperatorSQL($column, $operator, $bindIndex);
return parent::getOperatorSQL($column, $operator, $binds);
}

public function getSupportForTTLIndexes(): bool
Expand Down
Loading
Loading