diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 533673edb..a6233d189 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -1312,12 +1312,12 @@ public function increaseDocumentAttribute( $name = $this->filter($collection); $attribute = $this->filter($attribute); - $sqlMax = $max ? " AND `{$attribute}` <= {$max}" : ''; - $sqlMin = $min ? " AND `{$attribute}` >= {$min}" : ''; + $sqlMax = $max !== null ? " AND `{$attribute}` <= :max" : ''; + $sqlMin = $min !== null ? " AND `{$attribute}` >= :min" : ''; $sql = " - UPDATE {$this->getSQLTable($name)} - SET + UPDATE {$this->getSQLTable($name)} + SET `{$attribute}` = `{$attribute}` + :val, `_updatedAt` = :updatedAt WHERE _uid = :_uid @@ -1333,6 +1333,12 @@ public function increaseDocumentAttribute( $stmt->bindValue(':val', $value); $stmt->bindValue(':updatedAt', $updatedAt); + if ($max !== null) { + $stmt->bindValue(':max', $max); + } + if ($min !== null) { + $stmt->bindValue(':min', $min); + } if ($this->sharedTables) { $stmt->bindValue(':_tenant', $this->tenant); } diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index 5c5aada43..1a7b8e29f 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -1706,12 +1706,14 @@ public function increaseDocumentAttribute(string $collection, string $id, string $filters['_tenant'] = $this->getTenantFilters($collection); } - if ($max) { - $filters[$attribute] = ['$lte' => $max]; - } - - if ($min) { - $filters[$attribute] = ['$gte' => $min]; + if ($max !== null || $min !== null) { + $filters[$attribute] = []; + if ($max !== null) { + $filters[$attribute]['$lte'] = $max; + } + if ($min !== null) { + $filters[$attribute]['$gte'] = $min; + } } $options = $this->getTransactionOptions(); diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index bcc12c90b..6828f6324 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -1433,12 +1433,12 @@ public function increaseDocumentAttribute(string $collection, string $id, string $name = $this->filter($collection); $attribute = $this->filter($attribute); - $sqlMax = $max ? " AND \"{$attribute}\" <= {$max}" : ""; - $sqlMin = $min ? " AND \"{$attribute}\" >= {$min}" : ""; + $sqlMax = $max !== null ? " AND \"{$attribute}\" <= :max" : ""; + $sqlMin = $min !== null ? " AND \"{$attribute}\" >= :min" : ""; $sql = " - UPDATE {$this->getSQLTable($name)} - SET + UPDATE {$this->getSQLTable($name)} + SET \"{$attribute}\" = \"{$attribute}\" + :val, \"_updatedAt\" = :updatedAt WHERE _uid = :_uid @@ -1454,6 +1454,12 @@ public function increaseDocumentAttribute(string $collection, string $id, string $stmt->bindValue(':val', $value); $stmt->bindValue(':updatedAt', $updatedAt); + if ($max !== null) { + $stmt->bindValue(':max', $max); + } + if ($min !== null) { + $stmt->bindValue(':min', $min); + } if ($this->sharedTables) { $stmt->bindValue(':_tenant', $this->tenant); } diff --git a/src/Database/Database.php b/src/Database/Database.php index c343191b5..fa1718b54 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -4464,15 +4464,17 @@ public function createDocument(string $collection, Document $document): Document } } - $structure = new Structure( - $collection, - $this->adapter->getIdAttributeType(), - $this->adapter->getMinDateTime(), - $this->adapter->getMaxDateTime(), - $this->adapter->getSupportForAttributes() - ); - if (!$structure->isValid($document)) { - throw new StructureException($structure->getDescription()); + if ($this->validate) { + $structure = new Structure( + $collection, + $this->adapter->getIdAttributeType(), + $this->adapter->getMinDateTime(), + $this->adapter->getMaxDateTime(), + $this->adapter->getSupportForAttributes() + ); + if (!$structure->isValid($document)) { + throw new StructureException($structure->getDescription()); + } } $document = $this->adapter->castingBefore($collection, $document); @@ -4565,15 +4567,17 @@ public function createDocuments( $document = $this->encode($collection, $document); - $validator = new Structure( - $collection, - $this->adapter->getIdAttributeType(), - $this->adapter->getMinDateTime(), - $this->adapter->getMaxDateTime(), - $this->adapter->getSupportForAttributes() - ); - if (!$validator->isValid($document)) { - throw new StructureException($validator->getDescription()); + if ($this->validate) { + $validator = new Structure( + $collection, + $this->adapter->getIdAttributeType(), + $this->adapter->getMinDateTime(), + $this->adapter->getMaxDateTime(), + $this->adapter->getSupportForAttributes() + ); + if (!$validator->isValid($document)) { + throw new StructureException($validator->getDescription()); + } } if ($this->resolveRelationships) { @@ -5127,16 +5131,18 @@ public function updateDocument(string $collection, string $id, Document $documen $document = $this->encode($collection, $document); - $structureValidator = new Structure( - $collection, - $this->adapter->getIdAttributeType(), - $this->adapter->getMinDateTime(), - $this->adapter->getMaxDateTime(), - $this->adapter->getSupportForAttributes(), - $old - ); - if (!$structureValidator->isValid($document)) { // Make sure updated structure still apply collection rules (if any) - throw new StructureException($structureValidator->getDescription()); + if ($this->validate) { + $structureValidator = new Structure( + $collection, + $this->adapter->getIdAttributeType(), + $this->adapter->getMinDateTime(), + $this->adapter->getMaxDateTime(), + $this->adapter->getSupportForAttributes(), + $old + ); + if (!$structureValidator->isValid($document)) { // Make sure updated structure still apply collection rules (if any) + throw new StructureException($structureValidator->getDescription()); + } } if ($this->resolveRelationships) { @@ -5282,17 +5288,19 @@ public function updateDocuments( applyDefaults: false ); - $validator = new PartialStructure( - $collection, - $this->adapter->getIdAttributeType(), - $this->adapter->getMinDateTime(), - $this->adapter->getMaxDateTime(), - $this->adapter->getSupportForAttributes(), - null // No old document available in bulk updates - ); + if ($this->validate) { + $validator = new PartialStructure( + $collection, + $this->adapter->getIdAttributeType(), + $this->adapter->getMinDateTime(), + $this->adapter->getMaxDateTime(), + $this->adapter->getSupportForAttributes(), + null // No old document available in bulk updates + ); - if (!$validator->isValid($updates)) { - throw new StructureException($validator->getDescription()); + if (!$validator->isValid($updates)) { + throw new StructureException($validator->getDescription()); + } } $originalLimit = $limit; @@ -6046,17 +6054,19 @@ public function upsertDocumentsWithIncrease( } } - $validator = new Structure( - $collection, - $this->adapter->getIdAttributeType(), - $this->adapter->getMinDateTime(), - $this->adapter->getMaxDateTime(), - $this->adapter->getSupportForAttributes(), - $old->isEmpty() ? null : $old - ); + if ($this->validate) { + $validator = new Structure( + $collection, + $this->adapter->getIdAttributeType(), + $this->adapter->getMinDateTime(), + $this->adapter->getMaxDateTime(), + $this->adapter->getSupportForAttributes(), + $old->isEmpty() ? null : $old + ); - if (!$validator->isValid($document)) { - throw new StructureException($validator->getDescription()); + if (!$validator->isValid($document)) { + throw new StructureException($validator->getDescription()); + } } $document = $this->encode($collection, $document); diff --git a/tests/e2e/Adapter/Scopes/DocumentTests.php b/tests/e2e/Adapter/Scopes/DocumentTests.php index 1329ad1ee..f94dbbfb1 100644 --- a/tests/e2e/Adapter/Scopes/DocumentTests.php +++ b/tests/e2e/Adapter/Scopes/DocumentTests.php @@ -6185,4 +6185,182 @@ public function testCreateUpdateDocumentsMismatch(): void $database->deleteCollection($colName); } + public function testBypassStructureWithSupportForAttributes(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + // for schemaless the validation will be automatically skipped + if (!$database->getAdapter()->getSupportForAttributes()) { + $this->expectNotToPerformAssertions(); + return; + } + + $collectionId = 'successive_update_single'; + + $database->createCollection($collectionId); + $database->createAttribute($collectionId, 'attrA', Database::VAR_STRING, 50, true); + $database->createAttribute($collectionId, 'attrB', Database::VAR_STRING, 50, true); + + // bypass required + $database->disableValidation(); + + $permissions = [Permission::read(Role::any()), Permission::write(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any())]; + $docs = $database->createDocuments($collectionId, [ + new Document(['attrA' => null,'attrB' => 'B','$permissions' => $permissions]) + ]); + + $docs = $database->find($collectionId); + foreach ($docs as $doc) { + $this->assertArrayHasKey('attrA', $doc->getAttributes()); + $this->assertNull($doc->getAttribute('attrA')); + $this->assertEquals('B', $doc->getAttribute('attrB')); + } + // reset + $database->enableValidation(); + + try { + $database->createDocuments($collectionId, [ + new Document(['attrA' => null,'attrB' => 'B','$permissions' => $permissions]) + ]); + $this->fail('Failed to throw exception'); + } catch (Exception $e) { + $this->assertInstanceOf(StructureException::class, $e); + } + + $database->deleteCollection($collectionId); + } + + public function testValidationGuardsWithNullRequired(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + + if (!$database->getAdapter()->getSupportForAttributes()) { + $this->expectNotToPerformAssertions(); + return; + } + + // Base collection and attributes + $collection = 'validation_guard_all'; + $database->createCollection($collection, permissions: [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], documentSecurity: true); + $database->createAttribute($collection, 'name', Database::VAR_STRING, 32, true); + $database->createAttribute($collection, 'age', Database::VAR_INTEGER, 0, true); + $database->createAttribute($collection, 'value', Database::VAR_INTEGER, 0, false); + + // 1) createDocument with null required should fail when validation enabled, pass when disabled + try { + $database->createDocument($collection, new Document([ + '$permissions' => [Permission::read(Role::any()), Permission::create(Role::any())], + 'name' => null, + 'age' => null, + ])); + $this->fail('Failed to throw exception'); + } catch (Throwable $e) { + $this->assertInstanceOf(StructureException::class, $e); + } + + $database->disableValidation(); + $doc = $database->createDocument($collection, new Document([ + '$id' => 'created-null', + '$permissions' => [Permission::read(Role::any()), Permission::create(Role::any()), Permission::update(Role::any())], + 'name' => null, + 'age' => null, + ])); + $this->assertEquals('created-null', $doc->getId()); + $database->enableValidation(); + + // Seed a valid document for updates + $valid = $database->createDocument($collection, new Document([ + '$id' => 'valid', + '$permissions' => [Permission::read(Role::any()), Permission::update(Role::any())], + 'name' => 'ok', + 'age' => 10, + ])); + $this->assertEquals('valid', $valid->getId()); + + // 2) updateDocument set required to null should fail when validation enabled, pass when disabled + try { + $database->updateDocument($collection, 'valid', new Document([ + 'age' => null, + ])); + $this->fail('Failed to throw exception'); + } catch (Throwable $e) { + $this->assertInstanceOf(StructureException::class, $e); + } + + $database->disableValidation(); + $updated = $database->updateDocument($collection, 'valid', new Document([ + 'age' => null, + ])); + $this->assertNull($updated->getAttribute('age')); + $database->enableValidation(); + + // Seed a few valid docs for bulk update + for ($i = 0; $i < 2; $i++) { + $database->createDocument($collection, new Document([ + '$id' => 'b' . $i, + '$permissions' => [Permission::read(Role::any()), Permission::update(Role::any())], + 'name' => 'ok', + 'age' => 1, + ])); + } + + // 3) updateDocuments setting required to null should fail when validation enabled, pass when disabled + if ($database->getAdapter()->getSupportForBatchOperations()) { + try { + $database->updateDocuments($collection, new Document([ + 'name' => null, + ])); + $this->fail('Failed to throw exception'); + } catch (Throwable $e) { + $this->assertInstanceOf(StructureException::class, $e); + } + + $database->disableValidation(); + $count = $database->updateDocuments($collection, new Document([ + 'name' => null, + ])); + $this->assertGreaterThanOrEqual(3, $count); // at least the seeded docs are updated + $database->enableValidation(); + } + + // 4) upsertDocumentsWithIncrease with null required should fail when validation enabled, pass when disabled + if ($database->getAdapter()->getSupportForUpserts()) { + try { + $database->upsertDocumentsWithIncrease( + collection: $collection, + attribute: 'value', + documents: [new Document([ + '$id' => 'u1', + 'name' => null, // required null + 'value' => 1, + ])] + ); + $this->fail('Failed to throw exception'); + } catch (Throwable $e) { + $this->assertInstanceOf(StructureException::class, $e); + } + + $database->disableValidation(); + $ucount = $database->upsertDocumentsWithIncrease( + collection: $collection, + attribute: 'value', + documents: [new Document([ + '$id' => 'u1', + 'name' => null, + 'value' => 1, + ])] + ); + $this->assertEquals(1, $ucount); + $database->enableValidation(); + } + + // Cleanup + $database->deleteCollection($collection); + } }