@@ -2255,6 +2255,146 @@ public function testOperatorMultiplyViolatesRange(): void
22552255 $ database ->deleteCollection ($ collectionId );
22562256 }
22572257
2258+ /**
2259+ * Test MULTIPLY operator with negative multipliers and max limit
2260+ * Tests: Negative multipliers should not trigger incorrect overflow checks
2261+ */
2262+ public function testOperatorMultiplyWithNegativeMultiplier (): void
2263+ {
2264+ /** @var Database $database */
2265+ $ database = static ::getDatabase ();
2266+
2267+ $ collectionId = 'test_multiply_negative ' ;
2268+ $ database ->createCollection ($ collectionId );
2269+ $ database ->createAttribute ($ collectionId , 'value ' , Database::VAR_FLOAT , 0 , false );
2270+
2271+ // Test negative multiplier without max limit
2272+ $ doc1 = $ database ->createDocument ($ collectionId , new Document ([
2273+ '$id ' => 'negative_multiply ' ,
2274+ '$permissions ' => [Permission::read (Role::any ()), Permission::update (Role::any ())],
2275+ 'value ' => 10.0
2276+ ]));
2277+
2278+ $ updated1 = $ database ->updateDocument ($ collectionId , 'negative_multiply ' , new Document ([
2279+ 'value ' => Operator::multiply (-2 )
2280+ ]));
2281+ $ this ->assertEquals (-20.0 , $ updated1 ->getAttribute ('value ' ), 'Multiply by negative should work correctly ' );
2282+
2283+ // Test negative multiplier WITH max limit - should not incorrectly cap
2284+ $ doc2 = $ database ->createDocument ($ collectionId , new Document ([
2285+ '$id ' => 'negative_with_max ' ,
2286+ '$permissions ' => [Permission::read (Role::any ()), Permission::update (Role::any ())],
2287+ 'value ' => 10.0
2288+ ]));
2289+
2290+ $ updated2 = $ database ->updateDocument ($ collectionId , 'negative_with_max ' , new Document ([
2291+ 'value ' => Operator::multiply (-2 , 100 ) // max=100, but result will be -20
2292+ ]));
2293+ $ this ->assertEquals (-20.0 , $ updated2 ->getAttribute ('value ' ), 'Negative multiplier with max should not trigger overflow check ' );
2294+
2295+ // Test positive value * negative multiplier - result is negative, should not cap
2296+ $ doc3 = $ database ->createDocument ($ collectionId , new Document ([
2297+ '$id ' => 'pos_times_neg ' ,
2298+ '$permissions ' => [Permission::read (Role::any ()), Permission::update (Role::any ())],
2299+ 'value ' => 50.0
2300+ ]));
2301+
2302+ $ updated3 = $ database ->updateDocument ($ collectionId , 'pos_times_neg ' , new Document ([
2303+ 'value ' => Operator::multiply (-3 , 100 ) // 50 * -3 = -150, should not be capped at 100
2304+ ]));
2305+ $ this ->assertEquals (-150.0 , $ updated3 ->getAttribute ('value ' ), 'Positive * negative should compute correctly (result is negative, no cap) ' );
2306+
2307+ // Test negative value * negative multiplier that SHOULD hit max cap
2308+ $ doc4 = $ database ->createDocument ($ collectionId , new Document ([
2309+ '$id ' => 'negative_overflow ' ,
2310+ '$permissions ' => [Permission::read (Role::any ()), Permission::update (Role::any ())],
2311+ 'value ' => -60.0
2312+ ]));
2313+
2314+ $ updated4 = $ database ->updateDocument ($ collectionId , 'negative_overflow ' , new Document ([
2315+ 'value ' => Operator::multiply (-3 , 100 ) // -60 * -3 = 180, should be capped at 100
2316+ ]));
2317+ $ this ->assertEquals (100.0 , $ updated4 ->getAttribute ('value ' ), 'Negative * negative should cap at max when result would exceed it ' );
2318+
2319+ // Test zero multiplier with max
2320+ $ doc5 = $ database ->createDocument ($ collectionId , new Document ([
2321+ '$id ' => 'zero_multiply ' ,
2322+ '$permissions ' => [Permission::read (Role::any ()), Permission::update (Role::any ())],
2323+ 'value ' => 50.0
2324+ ]));
2325+
2326+ $ updated5 = $ database ->updateDocument ($ collectionId , 'zero_multiply ' , new Document ([
2327+ 'value ' => Operator::multiply (0 , 100 )
2328+ ]));
2329+ $ this ->assertEquals (0.0 , $ updated5 ->getAttribute ('value ' ), 'Multiply by zero should result in zero ' );
2330+
2331+ $ database ->deleteCollection ($ collectionId );
2332+ }
2333+
2334+ /**
2335+ * Test DIVIDE operator with negative divisors and min limit
2336+ * Tests: Negative divisors should not trigger incorrect underflow checks
2337+ */
2338+ public function testOperatorDivideWithNegativeDivisor (): void
2339+ {
2340+ /** @var Database $database */
2341+ $ database = static ::getDatabase ();
2342+
2343+ $ collectionId = 'test_divide_negative ' ;
2344+ $ database ->createCollection ($ collectionId );
2345+ $ database ->createAttribute ($ collectionId , 'value ' , Database::VAR_FLOAT , 0 , false );
2346+
2347+ // Test negative divisor without min limit
2348+ $ doc1 = $ database ->createDocument ($ collectionId , new Document ([
2349+ '$id ' => 'negative_divide ' ,
2350+ '$permissions ' => [Permission::read (Role::any ()), Permission::update (Role::any ())],
2351+ 'value ' => 20.0
2352+ ]));
2353+
2354+ $ updated1 = $ database ->updateDocument ($ collectionId , 'negative_divide ' , new Document ([
2355+ 'value ' => Operator::divide (-2 )
2356+ ]));
2357+ $ this ->assertEquals (-10.0 , $ updated1 ->getAttribute ('value ' ), 'Divide by negative should work correctly ' );
2358+
2359+ // Test negative divisor WITH min limit - should not incorrectly cap
2360+ $ doc2 = $ database ->createDocument ($ collectionId , new Document ([
2361+ '$id ' => 'negative_with_min ' ,
2362+ '$permissions ' => [Permission::read (Role::any ()), Permission::update (Role::any ())],
2363+ 'value ' => 20.0
2364+ ]));
2365+
2366+ $ updated2 = $ database ->updateDocument ($ collectionId , 'negative_with_min ' , new Document ([
2367+ 'value ' => Operator::divide (-2 , -50 ) // min=-50, result will be -10
2368+ ]));
2369+ $ this ->assertEquals (-10.0 , $ updated2 ->getAttribute ('value ' ), 'Negative divisor with min should not trigger underflow check ' );
2370+
2371+ // Test positive value / negative divisor - result is negative, should not cap at min
2372+ $ doc3 = $ database ->createDocument ($ collectionId , new Document ([
2373+ '$id ' => 'pos_div_neg ' ,
2374+ '$permissions ' => [Permission::read (Role::any ()), Permission::update (Role::any ())],
2375+ 'value ' => 100.0
2376+ ]));
2377+
2378+ $ updated3 = $ database ->updateDocument ($ collectionId , 'pos_div_neg ' , new Document ([
2379+ 'value ' => Operator::divide (-4 , -10 ) // 100 / -4 = -25, should not be capped at -10
2380+ ]));
2381+ $ this ->assertEquals (-25.0 , $ updated3 ->getAttribute ('value ' ), 'Positive / negative should compute correctly (result is negative, no cap) ' );
2382+
2383+ // Test negative value / negative divisor that would go below min
2384+ $ doc4 = $ database ->createDocument ($ collectionId , new Document ([
2385+ '$id ' => 'negative_underflow ' ,
2386+ '$permissions ' => [Permission::read (Role::any ()), Permission::update (Role::any ())],
2387+ 'value ' => 40.0
2388+ ]));
2389+
2390+ $ updated4 = $ database ->updateDocument ($ collectionId , 'negative_underflow ' , new Document ([
2391+ 'value ' => Operator::divide (-2 , -10 ) // 40 / -2 = -20, which is below min -10, so floor at -10
2392+ ]));
2393+ $ this ->assertEquals (-10.0 , $ updated4 ->getAttribute ('value ' ), 'Positive / negative should floor at min when result would be below it ' );
2394+
2395+ $ database ->deleteCollection ($ collectionId );
2396+ }
2397+
22582398 /**
22592399 * Bug #6: Post-Operator Validation Missing
22602400 * Test that ARRAY_APPEND can add items that violate array item constraints
@@ -3606,4 +3746,114 @@ public function testUpsertOperatorsOnNewDocuments(): void
36063746 // Cleanup
36073747 $ database ->deleteCollection ($ collectionId );
36083748 }
3749+
3750+ /**
3751+ * Test that array operators return empty arrays instead of NULL
3752+ * Tests: ARRAY_UNIQUE, ARRAY_INTERSECT, and ARRAY_DIFF return [] not NULL
3753+ */
3754+ public function testOperatorArrayEmptyResultsNotNull (): void
3755+ {
3756+ /** @var Database $database */
3757+ $ database = static ::getDatabase ();
3758+
3759+ $ collectionId = 'test_array_not_null ' ;
3760+ $ database ->createCollection ($ collectionId );
3761+ $ database ->createAttribute ($ collectionId , 'items ' , Database::VAR_STRING , 50 , false , null , true , true );
3762+
3763+ // Test ARRAY_UNIQUE on empty array returns [] not NULL
3764+ $ doc1 = $ database ->createDocument ($ collectionId , new Document ([
3765+ '$id ' => 'empty_unique ' ,
3766+ '$permissions ' => [Permission::read (Role::any ()), Permission::update (Role::any ())],
3767+ 'items ' => []
3768+ ]));
3769+
3770+ $ updated1 = $ database ->updateDocument ($ collectionId , 'empty_unique ' , new Document ([
3771+ 'items ' => Operator::arrayUnique ()
3772+ ]));
3773+ $ this ->assertIsArray ($ updated1 ->getAttribute ('items ' ), 'ARRAY_UNIQUE should return array not NULL ' );
3774+ $ this ->assertEquals ([], $ updated1 ->getAttribute ('items ' ), 'ARRAY_UNIQUE on empty array should return [] ' );
3775+
3776+ // Test ARRAY_INTERSECT with no matches returns [] not NULL
3777+ $ doc2 = $ database ->createDocument ($ collectionId , new Document ([
3778+ '$id ' => 'no_intersect ' ,
3779+ '$permissions ' => [Permission::read (Role::any ()), Permission::update (Role::any ())],
3780+ 'items ' => ['a ' , 'b ' , 'c ' ]
3781+ ]));
3782+
3783+ $ updated2 = $ database ->updateDocument ($ collectionId , 'no_intersect ' , new Document ([
3784+ 'items ' => Operator::arrayIntersect (['x ' , 'y ' , 'z ' ])
3785+ ]));
3786+ $ this ->assertIsArray ($ updated2 ->getAttribute ('items ' ), 'ARRAY_INTERSECT should return array not NULL ' );
3787+ $ this ->assertEquals ([], $ updated2 ->getAttribute ('items ' ), 'ARRAY_INTERSECT with no matches should return [] ' );
3788+
3789+ // Test ARRAY_DIFF removing all elements returns [] not NULL
3790+ $ doc3 = $ database ->createDocument ($ collectionId , new Document ([
3791+ '$id ' => 'diff_all ' ,
3792+ '$permissions ' => [Permission::read (Role::any ()), Permission::update (Role::any ())],
3793+ 'items ' => ['a ' , 'b ' , 'c ' ]
3794+ ]));
3795+
3796+ $ updated3 = $ database ->updateDocument ($ collectionId , 'diff_all ' , new Document ([
3797+ 'items ' => Operator::arrayDiff (['a ' , 'b ' , 'c ' ])
3798+ ]));
3799+ $ this ->assertIsArray ($ updated3 ->getAttribute ('items ' ), 'ARRAY_DIFF should return array not NULL ' );
3800+ $ this ->assertEquals ([], $ updated3 ->getAttribute ('items ' ), 'ARRAY_DIFF removing all elements should return [] ' );
3801+
3802+ // Cleanup
3803+ $ database ->deleteCollection ($ collectionId );
3804+ }
3805+
3806+ /**
3807+ * Test that updateDocuments with operators properly invalidates cache
3808+ * Tests: Cache should be purged after operator updates to prevent stale data
3809+ */
3810+ public function testUpdateDocumentsWithOperatorsCacheInvalidation (): void
3811+ {
3812+ /** @var Database $database */
3813+ $ database = static ::getDatabase ();
3814+
3815+ $ collectionId = 'test_operator_cache ' ;
3816+ $ database ->createCollection ($ collectionId );
3817+ $ database ->createAttribute ($ collectionId , 'counter ' , Database::VAR_INTEGER , 0 , false , 0 );
3818+
3819+ // Create a document
3820+ $ doc = $ database ->createDocument ($ collectionId , new Document ([
3821+ '$id ' => 'cache_test ' ,
3822+ '$permissions ' => [Permission::read (Role::any ()), Permission::update (Role::any ())],
3823+ 'counter ' => 10
3824+ ]));
3825+
3826+ // First read to potentially cache
3827+ $ fetched1 = $ database ->getDocument ($ collectionId , 'cache_test ' );
3828+ $ this ->assertEquals (10 , $ fetched1 ->getAttribute ('counter ' ));
3829+
3830+ // Use updateDocuments with operator
3831+ $ count = $ database ->updateDocuments (
3832+ $ collectionId ,
3833+ new Document ([
3834+ 'counter ' => Operator::increment (5 )
3835+ ]),
3836+ [Query::equal ('$id ' , ['cache_test ' ])]
3837+ );
3838+
3839+ $ this ->assertEquals (1 , $ count );
3840+
3841+ // Read again - should get fresh value, not cached old value
3842+ $ fetched2 = $ database ->getDocument ($ collectionId , 'cache_test ' );
3843+ $ this ->assertEquals (15 , $ fetched2 ->getAttribute ('counter ' ), 'Cache should be invalidated after operator update ' );
3844+
3845+ // Do another operator update
3846+ $ database ->updateDocuments (
3847+ $ collectionId ,
3848+ new Document ([
3849+ 'counter ' => Operator::multiply (2 )
3850+ ])
3851+ );
3852+
3853+ // Verify cache was invalidated again
3854+ $ fetched3 = $ database ->getDocument ($ collectionId , 'cache_test ' );
3855+ $ this ->assertEquals (30 , $ fetched3 ->getAttribute ('counter ' ), 'Cache should be invalidated after second operator update ' );
3856+
3857+ $ database ->deleteCollection ($ collectionId );
3858+ }
36093859}
0 commit comments