@@ -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 :
0 commit comments