From 2e8576e0ac48e0a50a0ea76f86c480bed095df27 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Tue, 4 Nov 2025 16:18:09 +0530 Subject: [PATCH 1/5] added validation check in createDocuments --- src/Database/Database.php | 20 ++++++----- tests/e2e/Adapter/Scopes/DocumentTests.php | 41 ++++++++++++++++++++++ 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index cf2c4cdf0..6e019edc9 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -4534,15 +4534,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) { diff --git a/tests/e2e/Adapter/Scopes/DocumentTests.php b/tests/e2e/Adapter/Scopes/DocumentTests.php index 3409175d7..2db191a8d 100644 --- a/tests/e2e/Adapter/Scopes/DocumentTests.php +++ b/tests/e2e/Adapter/Scopes/DocumentTests.php @@ -6185,4 +6185,45 @@ public function testCreateUpdateDocumentsMismatch(): void $database->deleteCollection($colName); } + public function testBypassStructureWithSupportForAttributes(): void + { + /** @var Database $database */ + $database = static::getDatabase(); + + $collectionId = 'successive_update_single'; + Authorization::cleanRoles(); + Authorization::setRole(Role::any()->toString()); + + $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); + } } From a95457011b9fabe578a6c61fb23067482d15854e Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Tue, 4 Nov 2025 16:28:45 +0530 Subject: [PATCH 2/5] updated tests --- tests/e2e/Adapter/Scopes/DocumentTests.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/e2e/Adapter/Scopes/DocumentTests.php b/tests/e2e/Adapter/Scopes/DocumentTests.php index 2db191a8d..503ea23d1 100644 --- a/tests/e2e/Adapter/Scopes/DocumentTests.php +++ b/tests/e2e/Adapter/Scopes/DocumentTests.php @@ -6189,6 +6189,11 @@ 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'; Authorization::cleanRoles(); From 89647afde74c9a707b21c178e11054b6398ebb3a Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Tue, 4 Nov 2025 16:35:38 +0530 Subject: [PATCH 3/5] updated tests --- tests/e2e/Adapter/Scopes/DocumentTests.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/e2e/Adapter/Scopes/DocumentTests.php b/tests/e2e/Adapter/Scopes/DocumentTests.php index 503ea23d1..8d4fe67ea 100644 --- a/tests/e2e/Adapter/Scopes/DocumentTests.php +++ b/tests/e2e/Adapter/Scopes/DocumentTests.php @@ -6196,8 +6196,6 @@ public function testBypassStructureWithSupportForAttributes(): void } $collectionId = 'successive_update_single'; - Authorization::cleanRoles(); - Authorization::setRole(Role::any()->toString()); $database->createCollection($collectionId); $database->createAttribute($collectionId, 'attrA', Database::VAR_STRING, 50, true); From 7290a34e44a83cfd58078be22b481533a5859b37 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Tue, 4 Nov 2025 17:04:18 +0530 Subject: [PATCH 4/5] added validation enable check for every db methods --- src/Database/Database.php | 86 +++++++------ tests/e2e/Adapter/Scopes/DocumentTests.php | 134 +++++++++++++++++++++ 2 files changed, 181 insertions(+), 39 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 6e019edc9..e348b2df6 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -4432,15 +4432,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); @@ -5096,16 +5098,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) { @@ -5252,17 +5256,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; @@ -6021,17 +6027,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 8d4fe67ea..867c39d86 100644 --- a/tests/e2e/Adapter/Scopes/DocumentTests.php +++ b/tests/e2e/Adapter/Scopes/DocumentTests.php @@ -6229,4 +6229,138 @@ public function testBypassStructureWithSupportForAttributes(): void $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); + } } From b35f6f399279ee2a391b02cf67fdd71a04de8556 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 5 Nov 2025 15:52:27 +1300 Subject: [PATCH 5/5] Fix min/max not bound --- src/Database/Adapter/MariaDB.php | 14 ++++++++++---- src/Database/Adapter/Mongo.php | 14 ++++++++------ src/Database/Adapter/Postgres.php | 14 ++++++++++---- 3 files changed, 28 insertions(+), 14 deletions(-) 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 1eaf59138..9fd57557f 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); }