Skip to content

Commit c9db3ae

Browse files
committed
Adress comments
1 parent e84f450 commit c9db3ae

2 files changed

Lines changed: 73 additions & 0 deletions

File tree

src/Database/Validator/Operator.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,25 @@ private function validateOperatorForAttribute(
221221
}
222222
}
223223

224+
// An unbounded power is computed directly by the database. Some engines hard-error on
225+
// results that are not real numbers, so reject those up-front with a clear message.
226+
if ($method === DatabaseOperator::TYPE_POWER && $this->currentDocument !== null && !isset($values[1])) {
227+
$base = (float)($this->currentDocument->getAttribute($operator->getAttribute()) ?? 0);
228+
$exponent = (float)$values[0];
229+
230+
// Zero raised to a negative power is undefined (a division by zero).
231+
if ($base === 0.0 && $exponent < 0) {
232+
$this->message = "Cannot apply power operator: zero raised to a negative power is undefined";
233+
return false;
234+
}
235+
236+
// A negative base raised to a fractional exponent is not a real number.
237+
if ($base < 0.0 && \floor($exponent) !== $exponent) {
238+
$this->message = "Cannot apply power operator: a negative base raised to a fractional exponent is undefined";
239+
return false;
240+
}
241+
}
242+
224243
break;
225244
case DatabaseOperator::TYPE_ARRAY_APPEND:
226245
case DatabaseOperator::TYPE_ARRAY_PREPEND:

tests/e2e/Adapter/Scopes/OperatorTests.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1651,6 +1651,60 @@ public function testOperatorPowerOnZeroOrNegativeBase(): void
16511651
$database->deleteCollection($collectionId);
16521652
}
16531653

1654+
/**
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.
1659+
*/
1660+
public function testOperatorUnboundedPowerOnUndefinedBase(): void
1661+
{
1662+
$database = static::getDatabase();
1663+
1664+
if (!$database->getAdapter()->getSupportForOperators()) {
1665+
$this->expectNotToPerformAssertions();
1666+
return;
1667+
}
1668+
1669+
$collectionId = 'operator_power_undefined';
1670+
$database->createCollection($collectionId);
1671+
$database->createAttribute($collectionId, 'value', Database::VAR_FLOAT, 0, false, 0.0);
1672+
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),
1682+
]));
1683+
$this->fail('Expected the update to be rejected for zero raised to a negative power');
1684+
} catch (StructureException) {
1685+
// expected
1686+
}
1687+
$this->assertEquals(0.0, $database->getDocument($collectionId, 'zero')->getAttribute('value'));
1688+
1689+
// The square root of a negative number is not a real number, so the update is rejected and -4 stays -4.
1690+
$database->createDocument($collectionId, new Document([
1691+
'$id' => 'neg',
1692+
'$permissions' => [Permission::read(Role::any()), Permission::update(Role::any())],
1693+
'value' => -4.0,
1694+
]));
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'));
1704+
1705+
$database->deleteCollection($collectionId);
1706+
}
1707+
16541708
/**
16551709
* Passing more values than the allowed maximum (10000) to an array operator must be rejected
16561710
* the same way on every adapter. Covers each operator that takes a caller-supplied value list.

0 commit comments

Comments
 (0)