55use Utopia \Database \Database ;
66use Utopia \Database \DateTime ;
77use Utopia \Database \Document ;
8+ use Utopia \Database \Exception \Limit as LimitException ;
89use Utopia \Database \Exception \Operator as OperatorException ;
910use Utopia \Database \Exception \Structure as StructureException ;
1011use Utopia \Database \Exception \Type as TypeException ;
@@ -1652,10 +1653,17 @@ public function testOperatorPowerOnZeroOrNegativeBase(): void
16521653 }
16531654
16541655 /**
1655- * A power with no maximum is computed directly by the database. When the result is not a real
1656- * number (zero raised to a negative power, or a negative base with a fractional exponent) some
1657- * databases throw a hard error. Validation rejects these before they reach the database, so the
1658- * update fails the same way on every adapter and the stored value is left untouched.
1656+ * A power with no maximum is computed directly by the engine on the live row value, so each
1657+ * adapter behaves as its engine does when the result is not a real number (zero raised to a
1658+ * negative power, or a negative base with a fractional exponent):
1659+ * - Most engines (MariaDB/MySQL/Postgres) and the in-memory adapters (Memory/Redis) reject it
1660+ * with a LimitException and leave the stored value untouched.
1661+ * - MongoDB rejects 0-to-a-negative-power the same way, but has no error for a negative base
1662+ * with a fractional exponent, so it stores NaN.
1663+ * - SQLite never raises on undefined math; it stores NULL (or leaves the value as-is).
1664+ *
1665+ * The one behaviour that must never happen on any adapter is silently storing a plausible but
1666+ * wrong real number, so the assertions verify the stored value via a fresh read.
16591667 */
16601668 public function testOperatorUnboundedPowerOnUndefinedBase (): void
16611669 {
@@ -1670,37 +1678,57 @@ public function testOperatorUnboundedPowerOnUndefinedBase(): void
16701678 $ database ->createCollection ($ collectionId );
16711679 $ database ->createAttribute ($ collectionId , 'value ' , Database::VAR_FLOAT , 0 , false , 0.0 );
16721680
1673- // 0 raised to a negative power is undefined, so the update is rejected and 0 stays 0.
1674- $ database ->createDocument ($ collectionId , new Document ([
1675- '$id ' => 'zero ' ,
1676- '$permissions ' => [Permission::read (Role::any ()), Permission::update (Role::any ())],
1677- 'value ' => 0.0 ,
1678- ]));
1679- try {
1680- $ database ->updateDocument ($ collectionId , 'zero ' , new Document ([
1681- 'value ' => Operator::power (-1 ),
1681+ // [id, starting value, operator]. Each result is mathematically undefined.
1682+ $ undefined = [
1683+ ['zero ' , 0.0 , Operator::power (-1 )], // 0 to a negative power
1684+ ['neg ' , -4.0 , Operator::power (0.5 )], // square root of a negative number
1685+ ];
1686+
1687+ foreach ($ undefined as [$ id , $ start , $ operator ]) {
1688+ $ database ->createDocument ($ collectionId , new Document ([
1689+ '$id ' => $ id ,
1690+ '$permissions ' => [Permission::read (Role::any ()), Permission::update (Role::any ())],
1691+ 'value ' => $ start ,
16821692 ]));
1683- $ this ->fail ('Expected the update to be rejected for zero raised to a negative power ' );
1684- } catch (StructureException ) {
1685- // expected
1693+
1694+
1695+ try {
1696+ $ caught = false ;
1697+ $ database ->updateDocument ($ collectionId , $ id , new Document (['value ' => $ operator ]));
1698+ } catch (\Throwable $ e ) {
1699+ $ caught = true ;
1700+ $ this ->assertInstanceOf (LimitException::class, $ e );
1701+
1702+ // Verify the actual stored value with a fresh read, not the returned document.
1703+ $ stored = $ database ->getDocument ($ collectionId , $ id )->getAttribute ('value ' );
1704+
1705+ // Whatever the engine raised must surface as a LimitException — not a raw
1706+ // PDO/Mongo/Json error. The row must also be left exactly as it was (the failed
1707+ // update is rolled back, no partial write).
1708+ $ this ->assertInstanceOf (LimitException::class, $ caught );
1709+ $ this ->assertEquals ($ start , $ stored , "{$ id }: value changed even though the update raised a LimitException " );
1710+ }
1711+
1712+ if ($ caught === false ) {
1713+ // Verify the actual stored value with a fresh read, not the returned document.
1714+ $ stored = $ database ->getDocument ($ collectionId , $ id )->getAttribute ('value ' );
1715+
1716+ // Engines that never raise on undefined math (SQLite, and MongoDB for a negative
1717+ // base) must still not store a wrong real number: the value is either untouched or
1718+ // an explicit "not a number" marker (NULL / NaN).
1719+ $ safe = $ stored === null || $ stored == $ start || !\is_finite ((float ) $ stored );
1720+ $ this ->assertTrue ($ safe , "{$ id }: undefined power neither raised a LimitException nor left a safe value; stored " . \var_export ($ stored , true ));
1721+ }
16861722 }
1687- $ this ->assertEquals (0.0 , $ database ->getDocument ($ collectionId , 'zero ' )->getAttribute ('value ' ));
16881723
1689- // The square root of a negative number is not a real number, so the update is rejected and -4 stays -4 .
1724+ // A valid unbounded power still computes normally on every adapter: 2^3 = 8 .
16901725 $ database ->createDocument ($ collectionId , new Document ([
1691- '$id ' => 'neg ' ,
1726+ '$id ' => 'valid ' ,
16921727 '$permissions ' => [Permission::read (Role::any ()), Permission::update (Role::any ())],
1693- 'value ' => - 4 .0 ,
1728+ 'value ' => 2 .0 ,
16941729 ]));
1695- try {
1696- $ database ->updateDocument ($ collectionId , 'neg ' , new Document ([
1697- 'value ' => Operator::power (0.5 ),
1698- ]));
1699- $ this ->fail ('Expected the update to be rejected for a negative base with a fractional exponent ' );
1700- } catch (StructureException ) {
1701- // expected
1702- }
1703- $ this ->assertEquals (-4.0 , $ database ->getDocument ($ collectionId , 'neg ' )->getAttribute ('value ' ));
1730+ $ database ->updateDocument ($ collectionId , 'valid ' , new Document (['value ' => Operator::power (3 )]));
1731+ $ this ->assertEquals (8.0 , $ database ->getDocument ($ collectionId , 'valid ' )->getAttribute ('value ' ));
17041732
17051733 $ database ->deleteCollection ($ collectionId );
17061734 }
0 commit comments