Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/Database/Adapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -757,7 +757,10 @@ abstract public function updateDocument(Document $collection, string $id, Docume
abstract public function updateDocuments(Document $collection, Document $updates, array $documents, array $skipPermissions = []): int;

/**
* Increase or decrease attribute value
* Increase or decrease attribute value, in one atomic write that changes nothing when the stored value is
* above $max or below $min
*
* @return bool False when the stored value was outside $min or $max, so nothing changed
*
* @throws Exception
*/
Expand Down
14 changes: 4 additions & 10 deletions src/Database/Adapter/Memory.php
Original file line number Diff line number Diff line change
Expand Up @@ -1912,30 +1912,24 @@ public function increaseDocumentAttribute(Document $collection, string $id, stri
$exact = (\is_int($current) || (\is_string($current) && BigInt::isIntegerString($current)))
&& (\is_int($value) || (\is_string($value) && BigInt::isIntegerString($value)));

// MariaDB encodes the bound check as part of the WHERE clause against
// the current column value (`attr <= :max` / `attr >= :min`); when the
// bound is violated the UPDATE simply matches zero rows and the call
// still returns true. Mirror that — silent no-op on bound violation.
// The Database layer pre-subtracts $value from $max (and adds it to
// $min), so the comparison stays against the pre-update value.
if ($exact) {
$current = BigInt::toNative($current);
$value = BigInt::toNative($value);
if (! is_null($min) && BigInt::compare($current, $min) < 0) {
return true;
return false;
}
if (! is_null($max) && BigInt::compare($current, $max) > 0) {
return true;
return false;
}
$result = BigInt::add($current, $value);
} else {
$current = $this->numericValue($current, 0) ?? 0;
$value = $this->numericValue($value, 0) ?? 0;
if (! is_null($min) && $current < $min) {
return true;
return false;
}
if (! is_null($max) && $current > $max) {
return true;
return false;
}
$result = $current + $value;
}
Expand Down
6 changes: 2 additions & 4 deletions src/Database/Adapter/Mongo.php
Original file line number Diff line number Diff line change
Expand Up @@ -2365,17 +2365,15 @@ public function increaseDocumentAttribute(Document $collection, string $id, stri
]]];

try {
$this->updateWithPipeline(
return $this->updateWithPipeline(
$this->getNamespace().'_'.$this->filter($collectionId),
$filters,
$pipeline,
$this->getTransactionOptions(),
);
) > 0;
} catch (MongoException $e) {
throw $this->processException($e);
}

return true;
}

private function normalizeAtomicNumber(int|float|string $value, string $name): int|float
Expand Down
8 changes: 4 additions & 4 deletions src/Database/Adapter/Redis.php
Original file line number Diff line number Diff line change
Expand Up @@ -1618,20 +1618,20 @@ public function increaseDocumentAttribute(Document $collection, string $id, stri
$current = BigInt::toNative($current);
$value = BigInt::toNative($value);
if (! \is_null($min) && BigInt::compare($current, $min) < 0) {
return true;
return false;
}
if (! \is_null($max) && BigInt::compare($current, $max) > 0) {
return true;
return false;
}
$result = BigInt::add($current, $value);
} else {
$current = $this->numericOr($current, 0);
$value = $this->numericOr($value, 0);
if (! \is_null($min) && $current < $min) {
return true;
return false;
}
if (! \is_null($max) && $current > $max) {
return true;
return false;
}
$result = $current + $value;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Database/Adapter/SQL.php
Original file line number Diff line number Diff line change
Expand Up @@ -1452,7 +1452,7 @@ public function increaseDocumentAttribute(
throw $this->processException($e);
}

return true;
return $statement->rowCount() > 0;
}

#[\Override]
Expand Down
83 changes: 75 additions & 8 deletions src/Database/Trait/Documents.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
use Utopia\Query\OrderDirection;
use Utopia\Query\Schema\ColumnType;
use Utopia\Query\Schema\IndexType;
use Utopia\Validator\Range;
use WeakMap;

trait Documents
Expand Down Expand Up @@ -287,6 +288,60 @@ private function numericAttribute(Collection $collection, string $key): ?Attribu
return $attribute;
}

/**
* The lower of the maximum an increase is given and the maximum the attribute's format holds it to.
*/
private function tighterMaximum(int|float|string|null $max, ?Attribute $attribute): int|float|string|null
{
$limit = $this->formatRange($attribute)?->getMax();
if ($limit === null || (\is_float($limit) && ! \is_finite($limit))) {
return $max;
}

if ($this->isDeclaredInteger($attribute)) {
$limit = $this->integerBound(Rounding::Floor->round($limit), 'Max');

return $max === null || BigInt::compare($limit, $max) < 0 ? $limit : $max;
}

return $max === null || $limit < $this->getNativeNumber($max) ? $limit : $max;
}

/**
* The higher of the minimum a decrease is given and the minimum the attribute's format holds it to.
*/
private function tighterMinimum(int|float|string|null $min, ?Attribute $attribute): int|float|string|null
{
$limit = $this->formatRange($attribute)?->getMin();
if ($limit === null || (\is_float($limit) && ! \is_finite($limit))) {
return $min;
}

if ($this->isDeclaredInteger($attribute)) {
$limit = $this->integerBound(Rounding::Ceil->round($limit), 'Min');

return $min === null || BigInt::compare($limit, $min) > 0 ? $limit : $min;
}

return $min === null || $limit > $this->getNativeNumber($min) ? $limit : $min;
}

/**
* The range an attribute's format holds its values to, when its format validator is a Range, as the
* `intRange`, `bigintRange` and `floatRange` formats Appwrite registers are.
*/
private function formatRange(?Attribute $attribute): ?Range
{
$format = $attribute?->format;
if ($attribute === null || $format === null || ! Structure::hasFormat($format->name, $attribute->type)) {
return null;
}

$validator = Structure::getFormat($format->name, $attribute->type)['callback']($attribute->toDocument()->getArrayCopy());

return $validator instanceof Range ? $validator : null;
}

private function isDeclaredInteger(?Attribute $attribute): bool
{
return $attribute !== null && ! $attribute->array && $attribute->isInteger();
Expand Down Expand Up @@ -2742,14 +2797,16 @@ public function increaseDocumentAttribute(

$collection = $this->silent(fn () => $this->getCollection($collection));
$numericAttribute = $this->numericAttribute($collection, $attribute);
$declared = $numericAttribute ?? $this->declaredAttribute($collection, $attribute);

if ($this->isDeclaredInteger($numericAttribute ?? $this->declaredAttribute($collection, $attribute))) {
if ($this->isDeclaredInteger($declared)) {
$value = Rounding::HalfAwayFromZero->round($value);
$this->assertIntegerChange($value);
if ($max !== null) {
$max = $this->integerBound(Rounding::Floor->round($max), 'Max');
}
}
$max = $this->tighterMaximum($max, $declared);

$cacheTarget = $collection->getId() === self::METADATA
? new Document([Document::ID => $id, Document::COLLECTION => self::METADATA])
Expand Down Expand Up @@ -2802,22 +2859,26 @@ public function increaseDocumentAttribute(
$time = DateTime::nowAfter($document->getUpdatedAt());
$updatedAt = $document->getUpdatedAt();
$updatedAt = (empty($updatedAt) || ! $this->datePreservation()->get()) ? $time : DateTime::setTimezone($updatedAt);
$storedMaximum = null;
if ($max !== null) {
$max = $numericAttribute?->isInteger() === true
$storedMaximum = $numericAttribute?->isInteger() === true
? BigInt::subtract($max, $value)
: $this->getNativeNumber($max) - $this->getNativeNumber($value);
}

$this->adapter->increaseDocumentAttribute(
$increased = $this->adapter->increaseDocumentAttribute(
$collection,
$id,
$attribute,
$numericAttribute?->isInteger() === true
? BigInt::toNative($value)
: $this->getNativeNumber($value),
$updatedAt,
max: $max
max: $storedMaximum
);
if (! $increased && $storedMaximum !== null) {
throw new LimitException('Attribute value exceeds maximum limit: '.$max);
}

$this->purgeCachedDocumentInternal($collection->getId(), $id);
$this->queueDocumentPurge($collection->getId(), $id);
Expand Down Expand Up @@ -2873,14 +2934,16 @@ public function decreaseDocumentAttribute(
$collection = $this->silent(fn () => $this->getCollection($collection));

$numericAttribute = $this->numericAttribute($collection, $attribute);
$declared = $numericAttribute ?? $this->declaredAttribute($collection, $attribute);

if ($this->isDeclaredInteger($numericAttribute ?? $this->declaredAttribute($collection, $attribute))) {
if ($this->isDeclaredInteger($declared)) {
$value = Rounding::HalfAwayFromZero->round($value);
$this->assertIntegerChange($value);
if ($min !== null) {
$min = $this->integerBound(Rounding::Ceil->round($min), 'Min');
}
}
$min = $this->tighterMinimum($min, $declared);

$cacheTarget = $collection->getId() === self::METADATA
? new Document([Document::ID => $id, Document::COLLECTION => self::METADATA])
Expand Down Expand Up @@ -2933,22 +2996,26 @@ public function decreaseDocumentAttribute(
$time = DateTime::nowAfter($document->getUpdatedAt());
$updatedAt = $document->getUpdatedAt();
$updatedAt = (empty($updatedAt) || ! $this->datePreservation()->get()) ? $time : DateTime::setTimezone($updatedAt);
$storedMinimum = null;
if ($min !== null) {
$min = $numericAttribute?->isInteger() === true
$storedMinimum = $numericAttribute?->isInteger() === true
? BigInt::add($min, $value)
: $this->getNativeNumber($min) + $this->getNativeNumber($value);
}

$this->adapter->increaseDocumentAttribute(
$decreased = $this->adapter->increaseDocumentAttribute(
$collection,
$id,
$attribute,
$numericAttribute?->isInteger() === true
? BigInt::negate($value)
: $this->getNativeNumber($value) * -1,
$updatedAt,
min: $min
min: $storedMinimum
);
if (! $decreased && $storedMinimum !== null) {
throw new LimitException('Attribute value exceeds minimum limit: '.$min);
}

$this->purgeCachedDocumentInternal($collection->getId(), $id);
$this->queueDocumentPurge($collection->getId(), $id);
Expand Down
Loading
Loading