diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 6d2aac8ef..59152a055 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -1228,13 +1228,15 @@ public function increaseDocumentAttribute( $name = $this->filter($collection); $attribute = $this->filter($attribute); - $sqlMax = $max !== null ? " AND `{$attribute}` <= :max" : ''; - $sqlMin = $min !== null ? " AND `{$attribute}` >= :min" : ''; + // A NULL counter (e.g. a column added to a table with existing rows) counts as 0, so it can + // be incremented and checked against its bounds instead of staying NULL forever. + $sqlMax = $max !== null ? " AND COALESCE(`{$attribute}`, 0) <= :max" : ''; + $sqlMin = $min !== null ? " AND COALESCE(`{$attribute}`, 0) >= :min" : ''; $sql = " UPDATE {$this->getSQLTable($name)} SET - `{$attribute}` = `{$attribute}` + :val, + `{$attribute}` = COALESCE(`{$attribute}`, 0) + :val, `_updatedAt` = :updatedAt WHERE _uid = :_uid {$this->getTenantQuery($collection)} diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index 760e9e79c..f1b8cbe3c 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -2338,9 +2338,18 @@ public function increaseDocumentAttribute(string $collection, string $id, string } $options = $this->getTransactionOptions(); + $namespace = $this->getNamespace() . '_' . $this->filter($collection); try { + // $inc refuses a null field, and a null never matches the bounds: a null counter (e.g. an + // attribute added after the document was written) counts as 0, as in the SQL adapters. + $nullFilters = ['_uid' => $id, $attribute => null]; + if ($this->sharedTables) { + $nullFilters['_tenant'] = $this->getTenantFilters($collection); + } + $this->client->update($namespace, $nullFilters, ['$set' => [$attribute => 0]], options: $options); + $this->client->update( - $this->getNamespace() . '_' . $this->filter($collection), + $namespace, $filters, [ '$inc' => [$attribute => $value], diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 3004e9780..2d820d488 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -1347,13 +1347,15 @@ public function increaseDocumentAttribute(string $collection, string $id, string $name = $this->filter($collection); $attribute = $this->filter($attribute); - $sqlMax = $max !== null ? " AND \"{$attribute}\" <= :max" : ""; - $sqlMin = $min !== null ? " AND \"{$attribute}\" >= :min" : ""; + // A NULL counter (e.g. a column added to a table with existing rows) counts as 0, so it can + // be incremented and checked against its bounds instead of staying NULL forever. + $sqlMax = $max !== null ? " AND COALESCE(\"{$attribute}\", 0) <= :max" : ""; + $sqlMin = $min !== null ? " AND COALESCE(\"{$attribute}\", 0) >= :min" : ""; $sql = " UPDATE {$this->getSQLTable($name)} SET - \"{$attribute}\" = \"{$attribute}\" + :val, + \"{$attribute}\" = COALESCE(\"{$attribute}\", 0) + :val, \"_updatedAt\" = :updatedAt WHERE _uid = :_uid {$this->getTenantQuery($collection)} diff --git a/tests/e2e/Adapter/Scopes/DocumentTests.php b/tests/e2e/Adapter/Scopes/DocumentTests.php index 2b6d37822..82ffd4cbe 100644 --- a/tests/e2e/Adapter/Scopes/DocumentTests.php +++ b/tests/e2e/Adapter/Scopes/DocumentTests.php @@ -2189,6 +2189,39 @@ public function testIncreaseDecrease(): Document return $document; } + public function testIncreaseDecreaseAttributeAddedAfterDocument(): void + { + /** @var Database $database */ + $database = $this->getDatabase(); + + $collection = 'increase_decrease_added_attribute'; + $database->createCollection($collection); + $this->assertEquals(true, $database->createAttribute($collection, 'name', Database::VAR_STRING, 64, false)); + + $document = $database->createDocument($collection, new Document([ + 'name' => 'existing', + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + ], + ])); + + // Added once the document exists: its stored value is NULL, not the default. + $this->assertEquals(true, $database->createAttribute($collection, 'counter', Database::VAR_INTEGER, 0, false, 0)); + + $doc = $database->increaseDocumentAttribute($collection, $document->getId(), 'counter', 1); + $this->assertEquals(1, $doc->getAttribute('counter')); + $this->assertEquals(1, $database->getDocument($collection, $document->getId())->getAttribute('counter')); + + $database->increaseDocumentAttribute($collection, $document->getId(), 'counter', 2, 10); + $this->assertEquals(3, $database->getDocument($collection, $document->getId())->getAttribute('counter')); + + $database->decreaseDocumentAttribute($collection, $document->getId(), 'counter', 1, 0); + $this->assertEquals(2, $database->getDocument($collection, $document->getId())->getAttribute('counter')); + + $database->deleteCollection($collection); + } + public function testCreateUpdateBigIntAndIncrementDecrement(): void { /** @var Database $database */