Skip to content

Commit 7bbe954

Browse files
authored
Merge pull request #909 from utopia-php/operators-binds
Operators binds
2 parents 6d5a86b + 820445b commit 7bbe954

5 files changed

Lines changed: 239 additions & 490 deletions

File tree

src/Database/Adapter/MariaDB.php

Lines changed: 81 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,7 +1037,7 @@ public function updateDocument(Document $collection, string $id, Document $docum
10371037
* Update Attributes
10381038
*/
10391039
$keyIndex = 0;
1040-
$opIndex = 0;
1040+
$operatorBinds = [];
10411041
$operators = [];
10421042

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

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

10861086
$keyIndex = 0;
1087-
$opIndexForBinding = 0;
10881087
foreach ($attributes as $attribute => $value) {
10891088
// Handle operators separately
10901089
if (isset($operators[$attribute])) {
1091-
$this->bindOperatorParams($stmt, $operators[$attribute], $opIndexForBinding);
1092-
} else {
1093-
// Convert spatial arrays to WKT, json_encode non-spatial arrays
1094-
if (\in_array($attribute, $spatialAttributes, true)) {
1095-
if (\is_array($value)) {
1096-
$value = $this->convertArrayToWKT($value);
1097-
}
1098-
} elseif (is_array($value)) {
1099-
$value = json_encode($value);
1100-
}
1090+
continue;
1091+
}
11011092

1102-
$bindKey = 'key_' . $keyIndex;
1103-
$value = (is_bool($value)) ? (int)$value : $value;
1104-
$stmt->bindValue(':' . $bindKey, $value, $this->getPDOType($value));
1105-
$keyIndex++;
1093+
// Convert spatial arrays to WKT, json_encode non-spatial arrays
1094+
if (\in_array($attribute, $spatialAttributes, true)) {
1095+
if (\is_array($value)) {
1096+
$value = $this->convertArrayToWKT($value);
1097+
}
1098+
} elseif (is_array($value)) {
1099+
$value = json_encode($value);
11061100
}
1101+
1102+
$bindKey = 'key_' . $keyIndex;
1103+
$value = (is_bool($value)) ? (int)$value : $value;
1104+
$stmt->bindValue(':' . $bindKey, $value, $this->getPDOType($value));
1105+
$keyIndex++;
1106+
}
1107+
1108+
foreach ($operatorBinds as $bindKey => $bindValue) {
1109+
$stmt->bindValue($bindKey, $bindValue, $this->getPDOType($bindValue));
11071110
}
11081111

11091112
$stmt->execute();
@@ -1159,7 +1162,7 @@ public function getUpsertStatement(
11591162
};
11601163

11611164
$updateColumns = [];
1162-
$opIndex = 0;
1165+
$operatorBinds = [];
11631166

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

11771180
if (isset($operators[$attr])) {
1178-
$operatorSQL = $this->getOperatorSQL($filteredAttr, $operators[$attr], $opIndex);
1181+
$operatorSQL = $this->getOperatorSQL($filteredAttr, $operators[$attr], $operatorBinds);
11791182
if ($operatorSQL !== null) {
11801183
$updateColumns[] = $operatorSQL;
11811184
}
@@ -1199,11 +1202,8 @@ public function getUpsertStatement(
11991202
$stmt->bindValue($key, $binding, $this->getPDOType($binding));
12001203
}
12011204

1202-
$opIndexForBinding = 0;
1203-
foreach (\array_keys($attributes) as $attr) {
1204-
if (isset($operators[$attr])) {
1205-
$this->bindOperatorParams($stmt, $operators[$attr], $opIndexForBinding);
1206-
}
1205+
foreach ($operatorBinds as $bindKey => $bindValue) {
1206+
$stmt->bindValue($bindKey, $bindValue, $this->getPDOType($bindValue));
12071207
}
12081208

12091209
return $stmt;
@@ -1951,10 +1951,10 @@ protected function quote(string $string): string
19511951
*
19521952
* @param string $column
19531953
* @param Operator $operator
1954-
* @param int &$bindIndex
1954+
* @param array<string, mixed> $binds
19551955
* @return ?string
19561956
*/
1957-
protected function getOperatorSQL(string $column, Operator $operator, int &$bindIndex): ?string
1957+
protected function getOperatorSQL(string $column, Operator $operator, array &$binds): ?string
19581958
{
19591959
$quotedColumn = $this->quote($column);
19601960
$method = $operator->getMethod();
@@ -1963,11 +1963,9 @@ protected function getOperatorSQL(string $column, Operator $operator, int &$bind
19631963
switch ($method) {
19641964
// Numeric operators
19651965
case Operator::TYPE_INCREMENT:
1966-
$bindKey = "op_{$bindIndex}";
1967-
$bindIndex++;
1966+
$bindKey = $this->registerOperatorBind($binds, $values[0] ?? 1);
19681967
if (isset($values[1])) {
1969-
$maxKey = "op_{$bindIndex}";
1970-
$bindIndex++;
1968+
$maxKey = $this->registerOperatorBind($binds, $values[1]);
19711969
return "{$quotedColumn} = CASE
19721970
WHEN COALESCE({$quotedColumn}, 0) >= :$maxKey THEN :$maxKey
19731971
WHEN COALESCE({$quotedColumn}, 0) > :$maxKey - :$bindKey THEN :$maxKey
@@ -1977,11 +1975,9 @@ protected function getOperatorSQL(string $column, Operator $operator, int &$bind
19771975
return "{$quotedColumn} = COALESCE({$quotedColumn}, 0) + :$bindKey";
19781976

19791977
case Operator::TYPE_DECREMENT:
1980-
$bindKey = "op_{$bindIndex}";
1981-
$bindIndex++;
1978+
$bindKey = $this->registerOperatorBind($binds, $values[0] ?? 1);
19821979
if (isset($values[1])) {
1983-
$minKey = "op_{$bindIndex}";
1984-
$bindIndex++;
1980+
$minKey = $this->registerOperatorBind($binds, $values[1]);
19851981
return "{$quotedColumn} = CASE
19861982
WHEN COALESCE({$quotedColumn}, 0) <= :$minKey THEN :$minKey
19871983
WHEN COALESCE({$quotedColumn}, 0) < :$minKey + :$bindKey THEN :$minKey
@@ -1991,11 +1987,9 @@ protected function getOperatorSQL(string $column, Operator $operator, int &$bind
19911987
return "{$quotedColumn} = COALESCE({$quotedColumn}, 0) - :$bindKey";
19921988

19931989
case Operator::TYPE_MULTIPLY:
1994-
$bindKey = "op_{$bindIndex}";
1995-
$bindIndex++;
1990+
$bindKey = $this->registerOperatorBind($binds, $values[0] ?? 1);
19961991
if (isset($values[1])) {
1997-
$maxKey = "op_{$bindIndex}";
1998-
$bindIndex++;
1992+
$maxKey = $this->registerOperatorBind($binds, $values[1]);
19991993
return "{$quotedColumn} = CASE
20001994
WHEN COALESCE({$quotedColumn}, 0) >= :$maxKey THEN :$maxKey
20011995
WHEN :$bindKey > 0 AND COALESCE({$quotedColumn}, 0) > :$maxKey / :$bindKey THEN :$maxKey
@@ -2006,11 +2000,9 @@ protected function getOperatorSQL(string $column, Operator $operator, int &$bind
20062000
return "{$quotedColumn} = COALESCE({$quotedColumn}, 0) * :$bindKey";
20072001

20082002
case Operator::TYPE_DIVIDE:
2009-
$bindKey = "op_{$bindIndex}";
2010-
$bindIndex++;
2003+
$bindKey = $this->registerOperatorBind($binds, $values[0] ?? 1);
20112004
if (isset($values[1])) {
2012-
$minKey = "op_{$bindIndex}";
2013-
$bindIndex++;
2005+
$minKey = $this->registerOperatorBind($binds, $values[1]);
20142006
return "{$quotedColumn} = CASE
20152007
WHEN :$bindKey != 0 AND COALESCE({$quotedColumn}, 0) / :$bindKey <= :$minKey THEN :$minKey
20162008
ELSE COALESCE({$quotedColumn}, 0) / :$bindKey
@@ -2019,16 +2011,13 @@ protected function getOperatorSQL(string $column, Operator $operator, int &$bind
20192011
return "{$quotedColumn} = COALESCE({$quotedColumn}, 0) / :$bindKey";
20202012

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

20262017
case Operator::TYPE_POWER:
2027-
$bindKey = "op_{$bindIndex}";
2028-
$bindIndex++;
2018+
$bindKey = $this->registerOperatorBind($binds, $values[0] ?? 1);
20292019
if (isset($values[1])) {
2030-
$maxKey = "op_{$bindIndex}";
2031-
$bindIndex++;
2020+
$maxKey = $this->registerOperatorBind($binds, $values[1]);
20322021
return "{$quotedColumn} = CASE
20332022
WHEN COALESCE({$quotedColumn}, 0) >= :$maxKey THEN :$maxKey
20342023
WHEN COALESCE({$quotedColumn}, 0) <= 1 THEN COALESCE({$quotedColumn}, 0)
@@ -2040,15 +2029,12 @@ protected function getOperatorSQL(string $column, Operator $operator, int &$bind
20402029

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

20472035
case Operator::TYPE_STRING_REPLACE:
2048-
$searchKey = "op_{$bindIndex}";
2049-
$bindIndex++;
2050-
$replaceKey = "op_{$bindIndex}";
2051-
$bindIndex++;
2036+
$searchKey = $this->registerOperatorBind($binds, $values[0] ?? '');
2037+
$replaceKey = $this->registerOperatorBind($binds, $values[1] ?? '');
20522038
return "{$quotedColumn} = REPLACE({$quotedColumn}, :$searchKey, :$replaceKey)";
20532039

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

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

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

20692059
case Operator::TYPE_ARRAY_INSERT:
2070-
$indexKey = "op_{$bindIndex}";
2071-
$bindIndex++;
2072-
$valueKey = "op_{$bindIndex}";
2073-
$bindIndex++;
2060+
$indexKey = $this->registerOperatorBind($binds, $values[0] ?? 0);
2061+
$valueKey = $this->registerOperatorBind($binds, json_encode($values[1] ?? null));
20742062
return "{$quotedColumn} = JSON_ARRAY_INSERT(
2075-
{$quotedColumn},
2076-
CONCAT('$[', :$indexKey, ']'),
2063+
{$quotedColumn},
2064+
CONCAT('$[', :$indexKey, ']'),
20772065
JSON_EXTRACT(:$valueKey, '$')
20782066
)";
20792067

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

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

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

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

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

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

21512154
case Operator::TYPE_DATE_SET_NOW:

src/Database/Adapter/MySQL.php

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -287,23 +287,28 @@ public function getSupportForOptionalSpatialAttributeWithExistingRows(): bool
287287
*
288288
* @param string $column
289289
* @param \Utopia\Database\Operator $operator
290-
* @param int &$bindIndex
290+
* @param array<string, mixed> $binds
291291
* @return ?string
292292
*/
293-
protected function getOperatorSQL(string $column, \Utopia\Database\Operator $operator, int &$bindIndex): ?string
293+
protected function getOperatorSQL(string $column, \Utopia\Database\Operator $operator, array &$binds): ?string
294294
{
295295
$quotedColumn = $this->quote($column);
296296
$method = $operator->getMethod();
297+
$values = $operator->getValues();
297298

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

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

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

319324
// For all other operators, use parent implementation
320-
return parent::getOperatorSQL($column, $operator, $bindIndex);
325+
return parent::getOperatorSQL($column, $operator, $binds);
321326
}
322327

323328
public function getSupportForTTLIndexes(): bool

0 commit comments

Comments
 (0)