Skip to content

Commit 590a784

Browse files
committed
Return new value atomically
1 parent 09196fb commit 590a784

4 files changed

Lines changed: 136 additions & 106 deletions

File tree

src/Database/Adapter.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1153,7 +1153,15 @@ protected function escapeWildcards(string $value): string
11531153
* @return bool
11541154
* @throws Exception
11551155
*/
1156-
abstract public function increaseDocumentAttribute(string $collection, string $id, string $attribute, int|float $value, string $updatedAt, int|float|null $min = null, int|float|null $max = null): bool;
1156+
abstract public function increaseDocumentAttribute(
1157+
string $collection,
1158+
string $id,
1159+
string $attribute,
1160+
int|float $value,
1161+
string $updatedAt,
1162+
int|float|null $min = null,
1163+
int|float|null $max = null
1164+
): bool;
11571165

11581166
/**
11591167
* Returns the connection ID identifier

src/Database/Adapter/MariaDB.php

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1382,8 +1382,15 @@ public function createOrUpdateDocuments(
13821382
* @return bool
13831383
* @throws DatabaseException
13841384
*/
1385-
public function increaseDocumentAttribute(string $collection, string $id, string $attribute, int|float $value, string $updatedAt, int|float|null $min = null, int|float|null $max = null): bool
1386-
{
1385+
public function increaseDocumentAttribute(
1386+
string $collection,
1387+
string $id,
1388+
string $attribute,
1389+
int|float $value,
1390+
string $updatedAt,
1391+
int|float|null $min = null,
1392+
int|float|null $max = null
1393+
): bool {
13871394
$name = $this->filter($collection);
13881395
$attribute = $this->filter($attribute);
13891396

@@ -1412,7 +1419,12 @@ public function increaseDocumentAttribute(string $collection, string $id, string
14121419
$stmt->bindValue(':_tenant', $this->tenant);
14131420
}
14141421

1415-
$stmt->execute() || throw new DatabaseException('Failed to update attribute');
1422+
try {
1423+
$stmt->execute();
1424+
} catch (PDOException $e) {
1425+
throw $this->processException($e);
1426+
}
1427+
14161428
return true;
14171429
}
14181430

src/Database/Database.php

Lines changed: 108 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -5091,44 +5091,32 @@ public function createOrUpdateDocumentsWithIncrease(
50915091
/**
50925092
* Increase a document attribute by a value
50935093
*
5094-
* @param string $collection
5095-
* @param string $id
5096-
* @param string $attribute
5097-
* @param int|float $value
5098-
* @param int|float|null $max
5099-
* @return bool
5100-
*
5094+
* @param string $collection The collection ID
5095+
* @param string $id The document ID
5096+
* @param string $attribute The attribute to increase
5097+
* @param int|float $value The value to increase the attribute by, can be a float
5098+
* @param int|float|null $max The maximum value the attribute can reach after the increase, null means no limit
5099+
* @return int The new value of the attribute after the increase
51015100
* @throws AuthorizationException
51025101
* @throws DatabaseException
5103-
* @throws Exception
5102+
* @throws LimitException
5103+
* @throws NotFoundException
5104+
* @throws TypeException
5105+
* @throws \Throwable
51045106
*/
5105-
public function increaseDocumentAttribute(string $collection, string $id, string $attribute, int|float $value = 1, int|float|null $max = null): bool
5106-
{
5107+
public function increaseDocumentAttribute(
5108+
string $collection,
5109+
string $id,
5110+
string $attribute,
5111+
int|float $value = 1,
5112+
int|float|null $max = null
5113+
): int|float {
51075114
if ($value <= 0) { // Can be a float
51085115
throw new DatabaseException('Value must be numeric and greater than 0');
51095116
}
51105117

5111-
$validator = new Authorization(self::PERMISSION_UPDATE);
5112-
5113-
/* @var $document Document */
5114-
$document = Authorization::skip(fn () => $this->silent(fn () => $this->getDocument($collection, $id))); // Skip ensures user does not need read permission for this
5115-
5116-
if ($document->isEmpty()) {
5117-
return false;
5118-
}
5119-
51205118
$collection = $this->silent(fn () => $this->getCollection($collection));
51215119

5122-
if ($collection->getId() !== self::METADATA) {
5123-
$documentSecurity = $collection->getAttribute('documentSecurity', false);
5124-
if (!$validator->isValid([
5125-
...$collection->getUpdate(),
5126-
...($documentSecurity ? $document->getUpdate() : [])
5127-
])) {
5128-
throw new AuthorizationException($validator->getDescription());
5129-
}
5130-
}
5131-
51325120
$attr = \array_filter($collection->getAttribute('attributes', []), function ($a) use ($attribute) {
51335121
return $a['$id'] === $attribute;
51345122
});
@@ -5137,46 +5125,63 @@ public function increaseDocumentAttribute(string $collection, string $id, string
51375125
throw new NotFoundException('Attribute not found');
51385126
}
51395127

5140-
$whiteList = [self::VAR_INTEGER, self::VAR_FLOAT];
5128+
$whiteList = [
5129+
self::VAR_INTEGER,
5130+
self::VAR_FLOAT
5131+
];
51415132

5142-
/**
5143-
* @var Document $attr
5144-
*/
5133+
/** @var Document $attr */
51455134
$attr = \end($attr);
51465135
if (!in_array($attr->getAttribute('type'), $whiteList)) {
51475136
throw new TypeException('Attribute type must be one of: ' . implode(',', $whiteList));
51485137
}
51495138

5150-
if ($max && ($document->getAttribute($attribute) + $value > $max)) {
5151-
throw new LimitException('Attribute value exceeds maximum limit: ' . $max);
5152-
}
5139+
$document = $this->withTransaction(function () use ($collection, $id, $attribute, $value, $max) {
5140+
/* @var $document Document */
5141+
$document = Authorization::skip(fn () => $this->silent(fn () => $this->getDocument($collection->getId(), $id, forUpdate: true))); // Skip ensures user does not need read permission for this
51535142

5154-
$time = DateTime::now();
5155-
$updatedAt = $document->getUpdatedAt();
5156-
$updatedAt = (empty($updatedAt) || !$this->preserveDates) ? $time : $updatedAt;
5143+
if ($document->isEmpty()) {
5144+
return false;
5145+
}
51575146

5158-
// Check if document was updated after the request timestamp
5159-
$oldUpdatedAt = new \DateTime($document->getUpdatedAt());
5160-
if (!is_null($this->timestamp) && $oldUpdatedAt > $this->timestamp) {
5161-
throw new ConflictException('Document was updated after the request timestamp');
5162-
}
5147+
$validator = new Authorization(self::PERMISSION_UPDATE);
5148+
5149+
if ($collection->getId() !== self::METADATA) {
5150+
$documentSecurity = $collection->getAttribute('documentSecurity', false);
5151+
if (!$validator->isValid([
5152+
...$collection->getUpdate(),
5153+
...($documentSecurity ? $document->getUpdate() : [])
5154+
])) {
5155+
throw new AuthorizationException($validator->getDescription());
5156+
}
5157+
}
51635158

5164-
$max = $max ? $max - $value : null;
5159+
if ($max && ($document->getAttribute($attribute) + $value > $max)) {
5160+
throw new LimitException('Attribute value exceeds maximum limit: ' . $max);
5161+
}
51655162

5166-
$result = $this->adapter->increaseDocumentAttribute(
5167-
$collection->getId(),
5168-
$id,
5169-
$attribute,
5170-
$value,
5171-
$updatedAt,
5172-
max: $max
5173-
);
5163+
$time = DateTime::now();
5164+
$updatedAt = $document->getUpdatedAt();
5165+
$updatedAt = (empty($updatedAt) || !$this->preserveDates) ? $time : $updatedAt;
5166+
$max = $max ? $max - $value : null;
5167+
5168+
$this->adapter->increaseDocumentAttribute(
5169+
$collection->getId(),
5170+
$id,
5171+
$attribute,
5172+
$value,
5173+
$updatedAt,
5174+
max: $max
5175+
);
5176+
5177+
return $document;
5178+
});
51745179

51755180
$this->purgeCachedDocument($collection->getId(), $id);
51765181

51775182
$this->trigger(self::EVENT_DOCUMENT_INCREASE, $document);
51785183

5179-
return $result;
5184+
return $document->getAttribute($attribute) + $value;
51805185
}
51815186

51825187

@@ -5188,38 +5193,24 @@ public function increaseDocumentAttribute(string $collection, string $id, string
51885193
* @param string $attribute
51895194
* @param int|float $value
51905195
* @param int|float|null $min
5191-
* @return bool
5196+
* @return int|float
51925197
*
51935198
* @throws AuthorizationException
51945199
* @throws DatabaseException
51955200
*/
5196-
public function decreaseDocumentAttribute(string $collection, string $id, string $attribute, int|float $value = 1, int|float|null $min = null): bool
5197-
{
5201+
public function decreaseDocumentAttribute(
5202+
string $collection,
5203+
string $id,
5204+
string $attribute,
5205+
int|float $value = 1,
5206+
int|float|null $min = null
5207+
): int|float {
51985208
if ($value <= 0) { // Can be a float
51995209
throw new DatabaseException('Value must be numeric and greater than 0');
52005210
}
52015211

5202-
$validator = new Authorization(self::PERMISSION_UPDATE);
5203-
5204-
/* @var $document Document */
5205-
$document = Authorization::skip(fn () => $this->silent(fn () => $this->getDocument($collection, $id))); // Skip ensures user does not need read permission for this
5206-
5207-
if ($document->isEmpty()) {
5208-
return false;
5209-
}
5210-
52115212
$collection = $this->silent(fn () => $this->getCollection($collection));
52125213

5213-
if ($collection->getId() !== self::METADATA) {
5214-
$documentSecurity = $collection->getAttribute('documentSecurity', false);
5215-
if (!$validator->isValid([
5216-
...$collection->getUpdate(),
5217-
...($documentSecurity ? $document->getUpdate() : [])
5218-
])) {
5219-
throw new AuthorizationException($validator->getDescription());
5220-
}
5221-
}
5222-
52235214
$attr = \array_filter($collection->getAttribute('attributes', []), function ($a) use ($attribute) {
52245215
return $a['$id'] === $attribute;
52255216
});
@@ -5228,7 +5219,10 @@ public function decreaseDocumentAttribute(string $collection, string $id, string
52285219
throw new NotFoundException('Attribute not found');
52295220
}
52305221

5231-
$whiteList = [self::VAR_INTEGER, self::VAR_FLOAT];
5222+
$whiteList = [
5223+
self::VAR_INTEGER,
5224+
self::VAR_FLOAT
5225+
];
52325226

52335227
/**
52345228
* @var Document $attr
@@ -5238,36 +5232,52 @@ public function decreaseDocumentAttribute(string $collection, string $id, string
52385232
throw new TypeException('Attribute type must be one of: ' . \implode(',', $whiteList));
52395233
}
52405234

5241-
if ($min && ($document->getAttribute($attribute) - $value < $min)) {
5242-
throw new LimitException('Attribute value exceeds minimum limit: ' . $min);
5243-
}
5235+
$document = $this->withTransaction(function () use ($collection, $id, $attribute, $value, $min) {
5236+
/* @var $document Document */
5237+
$document = Authorization::skip(fn () => $this->silent(fn () => $this->getDocument($collection->getId(), $id, forUpdate: true))); // Skip ensures user does not need read permission for this
52445238

5245-
$time = DateTime::now();
5246-
$updatedAt = $document->getUpdatedAt();
5247-
$updatedAt = (empty($updatedAt) || !$this->preserveDates) ? $time : $updatedAt;
5239+
if ($document->isEmpty()) {
5240+
return false;
5241+
}
52485242

5249-
// Check if document was updated after the request timestamp
5250-
$oldUpdatedAt = new \DateTime($document->getUpdatedAt());
5251-
if (!is_null($this->timestamp) && $oldUpdatedAt > $this->timestamp) {
5252-
throw new ConflictException('Document was updated after the request timestamp');
5253-
}
5243+
$validator = new Authorization(self::PERMISSION_UPDATE);
52545244

5255-
$min = $min ? $min + $value : null;
5245+
if ($collection->getId() !== self::METADATA) {
5246+
$documentSecurity = $collection->getAttribute('documentSecurity', false);
5247+
if (!$validator->isValid([
5248+
...$collection->getUpdate(),
5249+
...($documentSecurity ? $document->getUpdate() : [])
5250+
])) {
5251+
throw new AuthorizationException($validator->getDescription());
5252+
}
5253+
}
52565254

5257-
$result = $this->adapter->increaseDocumentAttribute(
5258-
$collection->getId(),
5259-
$id,
5260-
$attribute,
5261-
$value * -1,
5262-
$updatedAt,
5263-
min: $min
5264-
);
5255+
if ($min && ($document->getAttribute($attribute) - $value < $min)) {
5256+
throw new LimitException('Attribute value exceeds minimum limit: ' . $min);
5257+
}
5258+
5259+
$time = DateTime::now();
5260+
$updatedAt = $document->getUpdatedAt();
5261+
$updatedAt = (empty($updatedAt) || !$this->preserveDates) ? $time : $updatedAt;
5262+
$min = $min ? $min + $value : null;
5263+
5264+
$this->adapter->increaseDocumentAttribute(
5265+
$collection->getId(),
5266+
$id,
5267+
$attribute,
5268+
$value * -1,
5269+
$updatedAt,
5270+
min: $min
5271+
);
5272+
5273+
return $document;
5274+
});
52655275

52665276
$this->purgeCachedDocument($collection->getId(), $id);
52675277

52685278
$this->trigger(self::EVENT_DOCUMENT_DECREASE, $document);
52695279

5270-
return $result;
5280+
return $document->getAttribute($attribute) - $value;
52715281
}
52725282

52735283
/**

tests/e2e/Adapter/Scopes/DocumentTests.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -992,21 +992,21 @@ public function testIncreaseDecrease(): Document
992992

993993
$updatedAt = $document->getUpdatedAt();
994994

995-
$this->assertEquals(true, $database->increaseDocumentAttribute($collection, $document->getId(), 'increase', 1, 101));
995+
$this->assertEquals(101, $database->increaseDocumentAttribute($collection, $document->getId(), 'increase', 1, 101));
996996

997997
$document = $database->getDocument($collection, $document->getId());
998998
$this->assertEquals(101, $document->getAttribute('increase'));
999999
$this->assertNotEquals($updatedAt, $document->getUpdatedAt());
10001000

1001-
$this->assertEquals(true, $database->decreaseDocumentAttribute($collection, $document->getId(), 'decrease', 1, 98));
1001+
$this->assertEquals(99, $database->decreaseDocumentAttribute($collection, $document->getId(), 'decrease', 1, 98));
10021002
$document = $database->getDocument($collection, $document->getId());
10031003
$this->assertEquals(99, $document->getAttribute('decrease'));
10041004

1005-
$this->assertEquals(true, $database->increaseDocumentAttribute($collection, $document->getId(), 'increase_float', 5.5, 110));
1005+
$this->assertEquals(105.5, $database->increaseDocumentAttribute($collection, $document->getId(), 'increase_float', 5.5, 110));
10061006
$document = $database->getDocument($collection, $document->getId());
10071007
$this->assertEquals(105.5, $document->getAttribute('increase_float'));
10081008

1009-
$this->assertEquals(true, $database->decreaseDocumentAttribute($collection, $document->getId(), 'increase_float', 1.1, 100));
1009+
$this->assertEquals(104.4, $database->decreaseDocumentAttribute($collection, $document->getId(), 'increase_float', 1.1, 100));
10101010
$document = $database->getDocument($collection, $document->getId());
10111011
$this->assertEquals(104.4, $document->getAttribute('increase_float'));
10121012

0 commit comments

Comments
 (0)