From e4390b2e195a8f87469463b153140dbebbf9ca90 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 17 Jun 2026 10:03:25 +0300 Subject: [PATCH 01/18] validate texts --- src/Database/Database.php | 5 ++ src/Database/Validator/Structure.php | 11 ++++- tests/e2e/Adapter/Scopes/DocumentTests.php | 53 ++++++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 5a6cd97d7..27f2d3b84 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -115,6 +115,11 @@ class Database public const MAX_ARRAY_INDEX_LENGTH = 255; public const MAX_UID_DEFAULT_LENGTH = 36; + // Maximum byte capacity of TEXT-family columns (MySQL/MariaDB) + public const MAX_TEXT_BYTES = 65535; + public const MAX_MEDIUMTEXT_BYTES = 16777215; + public const MAX_LONGTEXT_BYTES = 4294967295; + // Min limits public const MIN_INT = -2147483648; diff --git a/src/Database/Validator/Structure.php b/src/Database/Validator/Structure.php index a3050b963..b582a5468 100644 --- a/src/Database/Validator/Structure.php +++ b/src/Database/Validator/Structure.php @@ -353,10 +353,19 @@ protected function checkForInvalidAttributeValues(Document $document, array $str $validators[] = new Sequence($this->idAttributeType, $attribute['$id'] === '$sequence'); break; - case Database::VAR_VARCHAR: case Database::VAR_TEXT: case Database::VAR_MEDIUMTEXT: case Database::VAR_LONGTEXT: + $maxBytes = match ($type) { + Database::VAR_MEDIUMTEXT => Database::MAX_MEDIUMTEXT_BYTES, + Database::VAR_LONGTEXT => Database::MAX_LONGTEXT_BYTES, + default => Database::MAX_TEXT_BYTES, + }; + $validators[] = new Text($size, min: 0); + $validators[] = new Text(\intdiv($maxBytes, 4), min: 0); + break; + + case Database::VAR_VARCHAR: case Database::VAR_STRING: $validators[] = new Text($size, min: 0); break; diff --git a/tests/e2e/Adapter/Scopes/DocumentTests.php b/tests/e2e/Adapter/Scopes/DocumentTests.php index 4f998372d..d7b1ec5f9 100644 --- a/tests/e2e/Adapter/Scopes/DocumentTests.php +++ b/tests/e2e/Adapter/Scopes/DocumentTests.php @@ -971,6 +971,59 @@ public function testUpsertDocuments(): void } } + public function testTextByteTruncation(): void + { + /** @var Database $database */ + $database = $this->getDatabase(); + + $database->createCollection(__FUNCTION__); + + // A `text` attribute at its maximum allowed size. On MySQL/MariaDB this + // maps to a TEXT column, which is limited to 65,535 *bytes*. + $database->createAttribute(__FUNCTION__, 'blocks_json', Database::VAR_TEXT, 65535, false); + + // The Structure validator caps a TEXT column at 65,535 / 4 = 16,383 + // characters (a utf8mb4 char is at most 4 bytes), guaranteeing the value + // fits the column's byte capacity. A 20,000-char value exceeds that and + // must be rejected up front with a clean StructureException, rather than + // letting the database raise error 1406 (data truncation). This applies + // to every write path; createDocument is the most basic. + $value = \str_repeat('📝', 20000); + $this->assertGreaterThan(16383, \mb_strlen($value)); // exceeds the byte-safe char limit + + $document = new Document([ + '$id' => 'first', + 'blocks_json' => $value, + '$permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + try { + $database->createDocument(__FUNCTION__, $document); + $this->fail('Expected StructureException for over-capacity text value'); + } catch (StructureException $e) { + $this->assertStringContainsString('16383 chars', $e->getMessage()); + } + + // A value within the byte-safe character limit is stored and round-trips intact. + $okValue = \str_repeat('a', 16383); + $created = $database->createDocument(__FUNCTION__, $document->setAttribute('blocks_json', $okValue)); + $fetched = $database->getDocument(__FUNCTION__, $created->getId()); + $this->assertEquals($okValue, $fetched->getAttribute('blocks_json')); + + // The same oversized value is also rejected on update. + try { + $database->updateDocument(__FUNCTION__, $created->getId(), $created->setAttribute('blocks_json', $value)); + $this->fail('Expected StructureException for over-capacity text value on update'); + } catch (StructureException $e) { + $this->assertStringContainsString('16383 chars', $e->getMessage()); + } + } + public function testUpsertDocumentsInc(): void { /** @var Database $database */ From cbdc3cf054e1481218f84995704636c817d9b80b Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 17 Jun 2026 10:03:35 +0300 Subject: [PATCH 02/18] validate texts --- tests/unit/Validator/StructureTest.php | 51 +++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/tests/unit/Validator/StructureTest.php b/tests/unit/Validator/StructureTest.php index 10afeaa0a..df4ea72e2 100644 --- a/tests/unit/Validator/StructureTest.php +++ b/tests/unit/Validator/StructureTest.php @@ -368,6 +368,55 @@ public function testStringValidation(): void $this->assertEquals('Invalid document structure: Attribute "title" has invalid type. Value must be a valid string and no longer than 256 chars', $validator->getDescription()); } + public function testTextByteSafeValidation(): void + { + // A legacy `text` attribute whose declared size (1MB) exceeds the real + // 65,535-byte capacity of a TEXT column. Such attributes exist in older + // databases created before VAR_TEXT was capped, so the limit must come + // from the column type, not from the (untrustworthy) declared size. + $collection = new Document([ + '$id' => ID::custom('posts'), + '$collection' => Database::METADATA, + 'name' => 'posts', + 'attributes' => [ + [ + '$id' => 'blocks_json', + 'type' => Database::VAR_TEXT, + 'format' => '', + 'size' => 1048576, + 'required' => false, + 'signed' => true, + 'array' => false, + 'filters' => [], + ], + ], + 'indexes' => [], + ]); + + $validator = new Structure($collection, Database::VAR_INTEGER); + + $base = [ + '$collection' => ID::custom('posts'), + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00', + ]; + + // The byte-safe character limit for a TEXT column is 65,535 / 4 = 16,383 + // (a utf8mb4 char is at most 4 bytes). Values longer than that are + // rejected even though the declared $size (1MB) would allow them. + $tooBig = \str_repeat('a', 16384); + $this->assertEquals(false, $validator->isValid(new Document($base + ['blocks_json' => $tooBig]))); + $this->assertEquals('Invalid document structure: Attribute "blocks_json" has invalid type. Value must be a valid string and no longer than 16383 chars', $validator->getDescription()); + + // Multi-byte content over the limit is rejected the same way. + $multibyte = \str_repeat('📝', 20000); + $this->assertEquals(false, $validator->isValid(new Document($base + ['blocks_json' => $multibyte]))); + + // A value within the byte-safe character limit is accepted. + $ok = \str_repeat('a', 16383); + $this->assertEquals(true, $validator->isValid(new Document($base + ['blocks_json' => $ok]))); + } + public function testArrayOfStringsValidation(): void { $validator = new Structure( @@ -959,7 +1008,7 @@ public function testTextValidation(): void 'published' => true, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', - 'text_field' => \str_repeat('a', 65535), + 'text_field' => \str_repeat('a', intdiv(65535, 4)), // byte-safe char cap for a TEXT column '$createdAt' => '2000-04-01T12:00:00.000+00:00', '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); From 99440b92735cd556eac885873b16c20cb318edf1 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 17 Jun 2026 17:01:38 +0300 Subject: [PATCH 03/18] validate texts --- tests/e2e/Adapter/Scopes/DocumentTests.php | 55 ++++++++++++++++++++-- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/tests/e2e/Adapter/Scopes/DocumentTests.php b/tests/e2e/Adapter/Scopes/DocumentTests.php index d7b1ec5f9..c0bc82ed2 100644 --- a/tests/e2e/Adapter/Scopes/DocumentTests.php +++ b/tests/e2e/Adapter/Scopes/DocumentTests.php @@ -971,7 +971,7 @@ public function testUpsertDocuments(): void } } - public function testTextByteTruncation(): void + public function testTextByteTruncationCreate(): void { /** @var Database $database */ $database = $this->getDatabase(); @@ -986,8 +986,7 @@ public function testTextByteTruncation(): void // characters (a utf8mb4 char is at most 4 bytes), guaranteeing the value // fits the column's byte capacity. A 20,000-char value exceeds that and // must be rejected up front with a clean StructureException, rather than - // letting the database raise error 1406 (data truncation). This applies - // to every write path; createDocument is the most basic. + // letting the database raise error 1406 (data truncation). $value = \str_repeat('📝', 20000); $this->assertGreaterThan(16383, \mb_strlen($value)); // exceeds the byte-safe char limit @@ -1008,14 +1007,60 @@ public function testTextByteTruncation(): void } catch (StructureException $e) { $this->assertStringContainsString('16383 chars', $e->getMessage()); } + } + + public function testTextByteTruncationValid(): void + { + /** @var Database $database */ + $database = $this->getDatabase(); + + $database->createCollection(__FUNCTION__); + $database->createAttribute(__FUNCTION__, 'blocks_json', Database::VAR_TEXT, 65535, false); // A value within the byte-safe character limit is stored and round-trips intact. $okValue = \str_repeat('a', 16383); - $created = $database->createDocument(__FUNCTION__, $document->setAttribute('blocks_json', $okValue)); + + $document = new Document([ + '$id' => 'first', + 'blocks_json' => $okValue, + '$permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $created = $database->createDocument(__FUNCTION__, $document); $fetched = $database->getDocument(__FUNCTION__, $created->getId()); $this->assertEquals($okValue, $fetched->getAttribute('blocks_json')); + } + + public function testTextByteTruncationUpdate(): void + { + /** @var Database $database */ + $database = $this->getDatabase(); + + $database->createCollection(__FUNCTION__); + $database->createAttribute(__FUNCTION__, 'blocks_json', Database::VAR_TEXT, 65535, false); + + $document = new Document([ + '$id' => 'first', + 'blocks_json' => \str_repeat('a', 16383), + '$permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $created = $database->createDocument(__FUNCTION__, $document); + + // An oversized value is rejected on update, the same as on create. + $value = \str_repeat('📝', 20000); + $this->assertGreaterThan(16383, \mb_strlen($value)); // exceeds the byte-safe char limit - // The same oversized value is also rejected on update. try { $database->updateDocument(__FUNCTION__, $created->getId(), $created->setAttribute('blocks_json', $value)); $this->fail('Expected StructureException for over-capacity text value on update'); From dde22feb6e216fd7c2e782f78408b1a999a5889f Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 17 Jun 2026 17:03:21 +0300 Subject: [PATCH 04/18] Structure test --- src/Database/Validator/Structure.php | 15 ++++++---- tests/unit/Validator/StructureTest.php | 40 +++++++++++++++++++++----- 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/src/Database/Validator/Structure.php b/src/Database/Validator/Structure.php index b582a5468..3a35ec167 100644 --- a/src/Database/Validator/Structure.php +++ b/src/Database/Validator/Structure.php @@ -354,15 +354,18 @@ protected function checkForInvalidAttributeValues(Document $document, array $str break; case Database::VAR_TEXT: + $validators[] = new Text($size, min: 0); + $validators[] = new Text(\intdiv(Database::MAX_TEXT_BYTES, 4), min: 0); + break; + case Database::VAR_MEDIUMTEXT: + $validators[] = new Text($size, min: 0); + $validators[] = new Text(\intdiv(Database::MAX_MEDIUMTEXT_BYTES, 4), min: 0); + break; + case Database::VAR_LONGTEXT: - $maxBytes = match ($type) { - Database::VAR_MEDIUMTEXT => Database::MAX_MEDIUMTEXT_BYTES, - Database::VAR_LONGTEXT => Database::MAX_LONGTEXT_BYTES, - default => Database::MAX_TEXT_BYTES, - }; $validators[] = new Text($size, min: 0); - $validators[] = new Text(\intdiv($maxBytes, 4), min: 0); + $validators[] = new Text(\intdiv(Database::MAX_LONGTEXT_BYTES, 4), min: 0); break; case Database::VAR_VARCHAR: diff --git a/tests/unit/Validator/StructureTest.php b/tests/unit/Validator/StructureTest.php index df4ea72e2..119e85506 100644 --- a/tests/unit/Validator/StructureTest.php +++ b/tests/unit/Validator/StructureTest.php @@ -368,12 +368,14 @@ public function testStringValidation(): void $this->assertEquals('Invalid document structure: Attribute "title" has invalid type. Value must be a valid string and no longer than 256 chars', $validator->getDescription()); } - public function testTextByteSafeValidation(): void + /** + * A legacy `text` attribute whose declared size (1MB) exceeds the real + * 65,535-byte capacity of a TEXT column. Such attributes exist in older + * databases created before VAR_TEXT was capped, so the limit must come + * from the column type, not from the (untrustworthy) declared size. + */ + private function getTextByteSafeValidator(): Structure { - // A legacy `text` attribute whose declared size (1MB) exceeds the real - // 65,535-byte capacity of a TEXT column. Such attributes exist in older - // databases created before VAR_TEXT was capped, so the limit must come - // from the column type, not from the (untrustworthy) declared size. $collection = new Document([ '$id' => ID::custom('posts'), '$collection' => Database::METADATA, @@ -393,13 +395,25 @@ public function testTextByteSafeValidation(): void 'indexes' => [], ]); - $validator = new Structure($collection, Database::VAR_INTEGER); + return new Structure($collection, Database::VAR_INTEGER); + } - $base = [ + /** + * @return array + */ + private function getTextByteSafeBase(): array + { + return [ '$collection' => ID::custom('posts'), '$createdAt' => '2000-04-01T12:00:00.000+00:00', '$updatedAt' => '2000-04-01T12:00:00.000+00:00', ]; + } + + public function testTextByteSafeValidationTooBig(): void + { + $validator = $this->getTextByteSafeValidator(); + $base = $this->getTextByteSafeBase(); // The byte-safe character limit for a TEXT column is 65,535 / 4 = 16,383 // (a utf8mb4 char is at most 4 bytes). Values longer than that are @@ -407,10 +421,22 @@ public function testTextByteSafeValidation(): void $tooBig = \str_repeat('a', 16384); $this->assertEquals(false, $validator->isValid(new Document($base + ['blocks_json' => $tooBig]))); $this->assertEquals('Invalid document structure: Attribute "blocks_json" has invalid type. Value must be a valid string and no longer than 16383 chars', $validator->getDescription()); + } + + public function testTextByteSafeValidationMultibyte(): void + { + $validator = $this->getTextByteSafeValidator(); + $base = $this->getTextByteSafeBase(); // Multi-byte content over the limit is rejected the same way. $multibyte = \str_repeat('📝', 20000); $this->assertEquals(false, $validator->isValid(new Document($base + ['blocks_json' => $multibyte]))); + } + + public function testTextByteSafeValidationValid(): void + { + $validator = $this->getTextByteSafeValidator(); + $base = $this->getTextByteSafeBase(); // A value within the byte-safe character limit is accepted. $ok = \str_repeat('a', 16383); From 29b8677609c5f777ddd117489ee00be07826b3b0 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 17 Jun 2026 17:12:58 +0300 Subject: [PATCH 05/18] change name --- tests/e2e/Adapter/Scopes/DocumentTests.php | 16 ++++++++-------- tests/unit/Validator/StructureTest.php | 10 +++++----- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/e2e/Adapter/Scopes/DocumentTests.php b/tests/e2e/Adapter/Scopes/DocumentTests.php index c0bc82ed2..58cf9e636 100644 --- a/tests/e2e/Adapter/Scopes/DocumentTests.php +++ b/tests/e2e/Adapter/Scopes/DocumentTests.php @@ -980,7 +980,7 @@ public function testTextByteTruncationCreate(): void // A `text` attribute at its maximum allowed size. On MySQL/MariaDB this // maps to a TEXT column, which is limited to 65,535 *bytes*. - $database->createAttribute(__FUNCTION__, 'blocks_json', Database::VAR_TEXT, 65535, false); + $database->createAttribute(__FUNCTION__, 'text', Database::VAR_TEXT, 65535, false); // The Structure validator caps a TEXT column at 65,535 / 4 = 16,383 // characters (a utf8mb4 char is at most 4 bytes), guaranteeing the value @@ -992,7 +992,7 @@ public function testTextByteTruncationCreate(): void $document = new Document([ '$id' => 'first', - 'blocks_json' => $value, + 'text' => $value, '$permissions' => [ Permission::read(Role::any()), Permission::create(Role::any()), @@ -1015,14 +1015,14 @@ public function testTextByteTruncationValid(): void $database = $this->getDatabase(); $database->createCollection(__FUNCTION__); - $database->createAttribute(__FUNCTION__, 'blocks_json', Database::VAR_TEXT, 65535, false); + $database->createAttribute(__FUNCTION__, 'text', Database::VAR_TEXT, 65535, false); // A value within the byte-safe character limit is stored and round-trips intact. $okValue = \str_repeat('a', 16383); $document = new Document([ '$id' => 'first', - 'blocks_json' => $okValue, + 'text' => $okValue, '$permissions' => [ Permission::read(Role::any()), Permission::create(Role::any()), @@ -1033,7 +1033,7 @@ public function testTextByteTruncationValid(): void $created = $database->createDocument(__FUNCTION__, $document); $fetched = $database->getDocument(__FUNCTION__, $created->getId()); - $this->assertEquals($okValue, $fetched->getAttribute('blocks_json')); + $this->assertEquals($okValue, $fetched->getAttribute('text')); } public function testTextByteTruncationUpdate(): void @@ -1042,11 +1042,11 @@ public function testTextByteTruncationUpdate(): void $database = $this->getDatabase(); $database->createCollection(__FUNCTION__); - $database->createAttribute(__FUNCTION__, 'blocks_json', Database::VAR_TEXT, 65535, false); + $database->createAttribute(__FUNCTION__, 'text', Database::VAR_TEXT, 65535, false); $document = new Document([ '$id' => 'first', - 'blocks_json' => \str_repeat('a', 16383), + 'text' => \str_repeat('a', 16383), '$permissions' => [ Permission::read(Role::any()), Permission::create(Role::any()), @@ -1062,7 +1062,7 @@ public function testTextByteTruncationUpdate(): void $this->assertGreaterThan(16383, \mb_strlen($value)); // exceeds the byte-safe char limit try { - $database->updateDocument(__FUNCTION__, $created->getId(), $created->setAttribute('blocks_json', $value)); + $database->updateDocument(__FUNCTION__, $created->getId(), $created->setAttribute('text', $value)); $this->fail('Expected StructureException for over-capacity text value on update'); } catch (StructureException $e) { $this->assertStringContainsString('16383 chars', $e->getMessage()); diff --git a/tests/unit/Validator/StructureTest.php b/tests/unit/Validator/StructureTest.php index 119e85506..1ee16f480 100644 --- a/tests/unit/Validator/StructureTest.php +++ b/tests/unit/Validator/StructureTest.php @@ -382,7 +382,7 @@ private function getTextByteSafeValidator(): Structure 'name' => 'posts', 'attributes' => [ [ - '$id' => 'blocks_json', + '$id' => 'text', 'type' => Database::VAR_TEXT, 'format' => '', 'size' => 1048576, @@ -419,8 +419,8 @@ public function testTextByteSafeValidationTooBig(): void // (a utf8mb4 char is at most 4 bytes). Values longer than that are // rejected even though the declared $size (1MB) would allow them. $tooBig = \str_repeat('a', 16384); - $this->assertEquals(false, $validator->isValid(new Document($base + ['blocks_json' => $tooBig]))); - $this->assertEquals('Invalid document structure: Attribute "blocks_json" has invalid type. Value must be a valid string and no longer than 16383 chars', $validator->getDescription()); + $this->assertEquals(false, $validator->isValid(new Document($base + ['text' => $tooBig]))); + $this->assertEquals('Invalid document structure: Attribute "text" has invalid type. Value must be a valid string and no longer than 16383 chars', $validator->getDescription()); } public function testTextByteSafeValidationMultibyte(): void @@ -430,7 +430,7 @@ public function testTextByteSafeValidationMultibyte(): void // Multi-byte content over the limit is rejected the same way. $multibyte = \str_repeat('📝', 20000); - $this->assertEquals(false, $validator->isValid(new Document($base + ['blocks_json' => $multibyte]))); + $this->assertEquals(false, $validator->isValid(new Document($base + ['text' => $multibyte]))); } public function testTextByteSafeValidationValid(): void @@ -440,7 +440,7 @@ public function testTextByteSafeValidationValid(): void // A value within the byte-safe character limit is accepted. $ok = \str_repeat('a', 16383); - $this->assertEquals(true, $validator->isValid(new Document($base + ['blocks_json' => $ok]))); + $this->assertEquals(true, $validator->isValid(new Document($base + ['text' => $ok]))); } public function testArrayOfStringsValidation(): void From 79c7d274af35e0984963c4ba17a5f0c184042684 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 17 Jun 2026 17:16:18 +0300 Subject: [PATCH 06/18] all inline --- tests/unit/Validator/StructureTest.php | 84 ++++++++++++++++++-------- 1 file changed, 59 insertions(+), 25 deletions(-) diff --git a/tests/unit/Validator/StructureTest.php b/tests/unit/Validator/StructureTest.php index 1ee16f480..11781cc6f 100644 --- a/tests/unit/Validator/StructureTest.php +++ b/tests/unit/Validator/StructureTest.php @@ -368,14 +368,12 @@ public function testStringValidation(): void $this->assertEquals('Invalid document structure: Attribute "title" has invalid type. Value must be a valid string and no longer than 256 chars', $validator->getDescription()); } - /** - * A legacy `text` attribute whose declared size (1MB) exceeds the real - * 65,535-byte capacity of a TEXT column. Such attributes exist in older - * databases created before VAR_TEXT was capped, so the limit must come - * from the column type, not from the (untrustworthy) declared size. - */ - private function getTextByteSafeValidator(): Structure + public function testTextByteSafeValidationTooBig(): void { + // A legacy `text` attribute whose declared size (1MB) exceeds the real + // 65,535-byte capacity of a TEXT column. Such attributes exist in older + // databases created before VAR_TEXT was capped, so the limit must come + // from the column type, not from the (untrustworthy) declared size. $collection = new Document([ '$id' => ID::custom('posts'), '$collection' => Database::METADATA, @@ -395,25 +393,13 @@ private function getTextByteSafeValidator(): Structure 'indexes' => [], ]); - return new Structure($collection, Database::VAR_INTEGER); - } + $validator = new Structure($collection, Database::VAR_INTEGER); - /** - * @return array - */ - private function getTextByteSafeBase(): array - { - return [ + $base = [ '$collection' => ID::custom('posts'), '$createdAt' => '2000-04-01T12:00:00.000+00:00', '$updatedAt' => '2000-04-01T12:00:00.000+00:00', ]; - } - - public function testTextByteSafeValidationTooBig(): void - { - $validator = $this->getTextByteSafeValidator(); - $base = $this->getTextByteSafeBase(); // The byte-safe character limit for a TEXT column is 65,535 / 4 = 16,383 // (a utf8mb4 char is at most 4 bytes). Values longer than that are @@ -425,8 +411,32 @@ public function testTextByteSafeValidationTooBig(): void public function testTextByteSafeValidationMultibyte(): void { - $validator = $this->getTextByteSafeValidator(); - $base = $this->getTextByteSafeBase(); + $collection = new Document([ + '$id' => ID::custom('posts'), + '$collection' => Database::METADATA, + 'name' => 'posts', + 'attributes' => [ + [ + '$id' => 'text', + 'type' => Database::VAR_TEXT, + 'format' => '', + 'size' => 1048576, + 'required' => false, + 'signed' => true, + 'array' => false, + 'filters' => [], + ], + ], + 'indexes' => [], + ]); + + $validator = new Structure($collection, Database::VAR_INTEGER); + + $base = [ + '$collection' => ID::custom('posts'), + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00', + ]; // Multi-byte content over the limit is rejected the same way. $multibyte = \str_repeat('📝', 20000); @@ -435,8 +445,32 @@ public function testTextByteSafeValidationMultibyte(): void public function testTextByteSafeValidationValid(): void { - $validator = $this->getTextByteSafeValidator(); - $base = $this->getTextByteSafeBase(); + $collection = new Document([ + '$id' => ID::custom('posts'), + '$collection' => Database::METADATA, + 'name' => 'posts', + 'attributes' => [ + [ + '$id' => 'text', + 'type' => Database::VAR_TEXT, + 'format' => '', + 'size' => 1048576, + 'required' => false, + 'signed' => true, + 'array' => false, + 'filters' => [], + ], + ], + 'indexes' => [], + ]); + + $validator = new Structure($collection, Database::VAR_INTEGER); + + $base = [ + '$collection' => ID::custom('posts'), + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00', + ]; // A value within the byte-safe character limit is accepted. $ok = \str_repeat('a', 16383); From 84e3950f744b187b7df28ac3934c6167afe14008 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 17 Jun 2026 17:36:44 +0300 Subject: [PATCH 07/18] Change inlines --- src/Database/Adapter/MariaDB.php | 4 ++-- src/Database/Adapter/SQLite.php | 6 +++--- src/Database/Database.php | 2 +- src/Database/Validator/Attribute.php | 12 ++++++------ src/Database/Validator/Structure.php | 5 ++++- 5 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 64e9681a2..4a15ae897 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -1609,11 +1609,11 @@ protected function getSQLType(string $type, int $size, bool $signed = true, bool case Database::VAR_STRING: // $size = $size * 4; // Convert utf8mb4 size to bytes - if ($size > 16777215) { + if ($size > Database::MAX_MEDIUMTEXT_BYTES) { return 'LONGTEXT'; } - if ($size > 65535) { + if ($size > Database::MAX_TEXT_BYTES) { return 'MEDIUMTEXT'; } diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index eb7e92b5b..a26fce85f 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -41,9 +41,9 @@ class SQLite extends MariaDB * introspection produces the same characterMaximumLength values that * INFORMATION_SCHEMA.COLUMNS would on MariaDB. */ - private const MARIADB_TEXT_BYTES = '65535'; - private const MARIADB_MEDIUMTEXT_BYTES = '16777215'; - private const MARIADB_LONGTEXT_BYTES = '4294967295'; + private const MARIADB_TEXT_BYTES = '' . Database::MAX_TEXT_BYTES; + private const MARIADB_MEDIUMTEXT_BYTES = '' . Database::MAX_MEDIUMTEXT_BYTES; + private const MARIADB_LONGTEXT_BYTES = '' . Database::MAX_LONGTEXT_BYTES; /** Suffix appended to every FTS5 virtual table name created by this adapter. */ private const FTS_TABLE_SUFFIX = '_fts'; diff --git a/src/Database/Database.php b/src/Database/Database.php index 27f2d3b84..d130734d9 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -115,7 +115,7 @@ class Database public const MAX_ARRAY_INDEX_LENGTH = 255; public const MAX_UID_DEFAULT_LENGTH = 36; - // Maximum byte capacity of TEXT-family columns (MySQL/MariaDB) + // Maximum byte capacity for TEXT public const MAX_TEXT_BYTES = 65535; public const MAX_MEDIUMTEXT_BYTES = 16777215; public const MAX_LONGTEXT_BYTES = 4294967295; diff --git a/src/Database/Validator/Attribute.php b/src/Database/Validator/Attribute.php index 143eaaf81..e3dc0044e 100644 --- a/src/Database/Validator/Attribute.php +++ b/src/Database/Validator/Attribute.php @@ -317,22 +317,22 @@ public function checkType(Document $attribute): bool break; case Database::VAR_TEXT: - if ($size > 65535) { - $this->message = 'Max size allowed for text is: 65535'; + if ($size > Database::MAX_TEXT_BYTES) { + $this->message = 'Max size allowed for text is: ' . Database::MAX_TEXT_BYTES; throw new DatabaseException($this->message); } break; case Database::VAR_MEDIUMTEXT: - if ($size > 16777215) { - $this->message = 'Max size allowed for mediumtext is: 16777215'; + if ($size > Database::MAX_MEDIUMTEXT_BYTES) { + $this->message = 'Max size allowed for mediumtext is: ' . Database::MAX_MEDIUMTEXT_BYTES; throw new DatabaseException($this->message); } break; case Database::VAR_LONGTEXT: - if ($size > 4294967295) { - $this->message = 'Max size allowed for longtext is: 4294967295'; + if ($size > Database::MAX_LONGTEXT_BYTES) { + $this->message = 'Max size allowed for longtext is: ' . Database::MAX_LONGTEXT_BYTES; throw new DatabaseException($this->message); } break; diff --git a/src/Database/Validator/Structure.php b/src/Database/Validator/Structure.php index 3a35ec167..cd9f72e38 100644 --- a/src/Database/Validator/Structure.php +++ b/src/Database/Validator/Structure.php @@ -365,7 +365,10 @@ protected function checkForInvalidAttributeValues(Document $document, array $str case Database::VAR_LONGTEXT: $validators[] = new Text($size, min: 0); - $validators[] = new Text(\intdiv(Database::MAX_LONGTEXT_BYTES, 4), min: 0); + // MAX_LONGTEXT_BYTES (4294967295) exceeds the 32-bit int max, so on + // 32-bit PHP it is a float and intdiv() would raise a TypeError. The + // /4 result (1073741823) fits a 32-bit int, so divide as float and cast. + $validators[] = new Text((int)(Database::MAX_LONGTEXT_BYTES / 4), min: 0); break; case Database::VAR_VARCHAR: From 44852f1c1b93fc2553c90615d1a8aa63512881b2 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 17 Jun 2026 17:48:19 +0300 Subject: [PATCH 08/18] line --- src/Database/Adapter/SQLite.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index a26fce85f..f06b219a2 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -50,7 +50,6 @@ class SQLite extends MariaDB /** AFTER INSERT trigger suffix on the parent collection. */ private const FTS_TRIGGER_INSERT = 'ai'; - /** AFTER DELETE trigger suffix on the parent collection. */ private const FTS_TRIGGER_DELETE = 'ad'; From 24cb9772afda94d7464c089da4f23321b0979e52 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 18 Jun 2026 10:04:01 +0300 Subject: [PATCH 09/18] 32 bit systems --- src/Database/Validator/Structure.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Database/Validator/Structure.php b/src/Database/Validator/Structure.php index cd9f72e38..3a35ec167 100644 --- a/src/Database/Validator/Structure.php +++ b/src/Database/Validator/Structure.php @@ -365,10 +365,7 @@ protected function checkForInvalidAttributeValues(Document $document, array $str case Database::VAR_LONGTEXT: $validators[] = new Text($size, min: 0); - // MAX_LONGTEXT_BYTES (4294967295) exceeds the 32-bit int max, so on - // 32-bit PHP it is a float and intdiv() would raise a TypeError. The - // /4 result (1073741823) fits a 32-bit int, so divide as float and cast. - $validators[] = new Text((int)(Database::MAX_LONGTEXT_BYTES / 4), min: 0); + $validators[] = new Text(\intdiv(Database::MAX_LONGTEXT_BYTES, 4), min: 0); break; case Database::VAR_VARCHAR: From 3dc94f95186c27c2fc6fa8b1bd97479eb0cbb57a Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 18 Jun 2026 12:22:52 +0300 Subject: [PATCH 10/18] Update main --- phpunit.xml | 2 +- src/Database/Validator/ByteLength.php | 53 ++++++++++++++++++++++ src/Database/Validator/Structure.php | 6 +-- tests/e2e/Adapter/Base.php | 4 +- tests/e2e/Adapter/Scopes/DocumentTests.php | 26 ++++++----- tests/unit/Validator/StructureTest.php | 19 ++++---- 6 files changed, 84 insertions(+), 26 deletions(-) create mode 100644 src/Database/Validator/ByteLength.php diff --git a/phpunit.xml b/phpunit.xml index 2a0531cfd..34365d48d 100755 --- a/phpunit.xml +++ b/phpunit.xml @@ -7,7 +7,7 @@ convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" - stopOnFailure="false" + stopOnFailure="true" > diff --git a/src/Database/Validator/ByteLength.php b/src/Database/Validator/ByteLength.php new file mode 100644 index 000000000..b26d10a96 --- /dev/null +++ b/src/Database/Validator/ByteLength.php @@ -0,0 +1,53 @@ +max = $max; + } + + public function getDescription(): string + { + return 'Value must be a valid string no longer than ' . $this->max . ' bytes'; + } + + public function isArray(): bool + { + return false; + } + + public function getType(): string + { + return self::TYPE_STRING; + } + + public function isValid(mixed $value): bool + { + if (!\is_string($value)) { + return false; + } + + if ($this->max !== 0 && \strlen($value) > $this->max) { + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/src/Database/Validator/Structure.php b/src/Database/Validator/Structure.php index 3a35ec167..63565e72e 100644 --- a/src/Database/Validator/Structure.php +++ b/src/Database/Validator/Structure.php @@ -355,17 +355,17 @@ protected function checkForInvalidAttributeValues(Document $document, array $str case Database::VAR_TEXT: $validators[] = new Text($size, min: 0); - $validators[] = new Text(\intdiv(Database::MAX_TEXT_BYTES, 4), min: 0); + $validators[] = new ByteLength(Database::MAX_TEXT_BYTES); break; case Database::VAR_MEDIUMTEXT: $validators[] = new Text($size, min: 0); - $validators[] = new Text(\intdiv(Database::MAX_MEDIUMTEXT_BYTES, 4), min: 0); + $validators[] = new ByteLength(Database::MAX_MEDIUMTEXT_BYTES); break; case Database::VAR_LONGTEXT: $validators[] = new Text($size, min: 0); - $validators[] = new Text(\intdiv(Database::MAX_LONGTEXT_BYTES, 4), min: 0); + $validators[] = new ByteLength(Database::MAX_LONGTEXT_BYTES); break; case Database::VAR_VARCHAR: diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 4baeba35b..fee253d35 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -23,8 +23,8 @@ abstract class Base extends TestCase { - use CollectionTests; - use CustomDocumentTypeTests; +// use CollectionTests; +// use CustomDocumentTypeTests; use DocumentTests; use AttributeTests; use IndexTests; diff --git a/tests/e2e/Adapter/Scopes/DocumentTests.php b/tests/e2e/Adapter/Scopes/DocumentTests.php index 58cf9e636..b3fc61e48 100644 --- a/tests/e2e/Adapter/Scopes/DocumentTests.php +++ b/tests/e2e/Adapter/Scopes/DocumentTests.php @@ -982,13 +982,14 @@ public function testTextByteTruncationCreate(): void // maps to a TEXT column, which is limited to 65,535 *bytes*. $database->createAttribute(__FUNCTION__, 'text', Database::VAR_TEXT, 65535, false); - // The Structure validator caps a TEXT column at 65,535 / 4 = 16,383 - // characters (a utf8mb4 char is at most 4 bytes), guaranteeing the value - // fits the column's byte capacity. A 20,000-char value exceeds that and - // must be rejected up front with a clean StructureException, rather than - // letting the database raise error 1406 (data truncation). + // The Structure validator caps a TEXT column at its 65,535-byte capacity, + // measuring the value's actual byte length. A 20,000-char emoji value is + // 80,000 bytes (4 bytes per char in utf8mb4), so it exceeds the column's + // byte capacity and must be rejected up front with a clean + // StructureException, rather than letting the database raise error 1406 + // (data truncation). $value = \str_repeat('📝', 20000); - $this->assertGreaterThan(16383, \mb_strlen($value)); // exceeds the byte-safe char limit + $this->assertGreaterThan(65535, \strlen($value)); // exceeds the byte capacity $document = new Document([ '$id' => 'first', @@ -1005,7 +1006,7 @@ public function testTextByteTruncationCreate(): void $database->createDocument(__FUNCTION__, $document); $this->fail('Expected StructureException for over-capacity text value'); } catch (StructureException $e) { - $this->assertStringContainsString('16383 chars', $e->getMessage()); + $this->assertStringContainsString('65535 bytes', $e->getMessage()); } } @@ -1017,8 +1018,11 @@ public function testTextByteTruncationValid(): void $database->createCollection(__FUNCTION__); $database->createAttribute(__FUNCTION__, 'text', Database::VAR_TEXT, 65535, false); - // A value within the byte-safe character limit is stored and round-trips intact. - $okValue = \str_repeat('a', 16383); + // A value that fills the column's full byte capacity is stored and + // round-trips intact. 65,535 ASCII chars are exactly 65,535 bytes, so + // byte-based validation accepts the whole column, where the previous + // char-based cap would have rejected anything over 16,383 chars. + $okValue = \str_repeat('a', 65535); $document = new Document([ '$id' => 'first', @@ -1059,13 +1063,13 @@ public function testTextByteTruncationUpdate(): void // An oversized value is rejected on update, the same as on create. $value = \str_repeat('📝', 20000); - $this->assertGreaterThan(16383, \mb_strlen($value)); // exceeds the byte-safe char limit + $this->assertGreaterThan(65535, \strlen($value)); // exceeds the byte capacity try { $database->updateDocument(__FUNCTION__, $created->getId(), $created->setAttribute('text', $value)); $this->fail('Expected StructureException for over-capacity text value on update'); } catch (StructureException $e) { - $this->assertStringContainsString('16383 chars', $e->getMessage()); + $this->assertStringContainsString('65535 bytes', $e->getMessage()); } } diff --git a/tests/unit/Validator/StructureTest.php b/tests/unit/Validator/StructureTest.php index 11781cc6f..bdd22916a 100644 --- a/tests/unit/Validator/StructureTest.php +++ b/tests/unit/Validator/StructureTest.php @@ -401,12 +401,12 @@ public function testTextByteSafeValidationTooBig(): void '$updatedAt' => '2000-04-01T12:00:00.000+00:00', ]; - // The byte-safe character limit for a TEXT column is 65,535 / 4 = 16,383 - // (a utf8mb4 char is at most 4 bytes). Values longer than that are - // rejected even though the declared $size (1MB) would allow them. - $tooBig = \str_repeat('a', 16384); + // A TEXT column is limited to 65,535 bytes. Validation measures the + // value's actual byte length, so a value over that capacity is rejected + // even though the declared $size (1MB) would allow the character count. + $tooBig = \str_repeat('a', 65536); $this->assertEquals(false, $validator->isValid(new Document($base + ['text' => $tooBig]))); - $this->assertEquals('Invalid document structure: Attribute "text" has invalid type. Value must be a valid string and no longer than 16383 chars', $validator->getDescription()); + $this->assertEquals('Invalid document structure: Attribute "text" has invalid type. Value must be a valid string no longer than 65535 bytes', $validator->getDescription()); } public function testTextByteSafeValidationMultibyte(): void @@ -438,7 +438,8 @@ public function testTextByteSafeValidationMultibyte(): void '$updatedAt' => '2000-04-01T12:00:00.000+00:00', ]; - // Multi-byte content over the limit is rejected the same way. + // Multi-byte content over the byte capacity is rejected the same way + // (20,000 emoji = 80,000 bytes in utf8mb4). $multibyte = \str_repeat('📝', 20000); $this->assertEquals(false, $validator->isValid(new Document($base + ['text' => $multibyte]))); } @@ -472,8 +473,8 @@ public function testTextByteSafeValidationValid(): void '$updatedAt' => '2000-04-01T12:00:00.000+00:00', ]; - // A value within the byte-safe character limit is accepted. - $ok = \str_repeat('a', 16383); + // A value that fills the column's full byte capacity is accepted. + $ok = \str_repeat('a', 65535); $this->assertEquals(true, $validator->isValid(new Document($base + ['text' => $ok]))); } @@ -1068,7 +1069,7 @@ public function testTextValidation(): void 'published' => true, 'tags' => ['dog', 'cat', 'mouse'], 'feedback' => 'team@appwrite.io', - 'text_field' => \str_repeat('a', intdiv(65535, 4)), // byte-safe char cap for a TEXT column + 'text_field' => \str_repeat('a', 65535), // fills the TEXT column's full byte capacity '$createdAt' => '2000-04-01T12:00:00.000+00:00', '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); From bcfad739896e95ffe09edbbbbe43f3bcd88d5c3e Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 18 Jun 2026 12:42:23 +0300 Subject: [PATCH 11/18] Check null values passes --- tests/unit/Validator/StructureTest.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/unit/Validator/StructureTest.php b/tests/unit/Validator/StructureTest.php index bdd22916a..b7a6cbf69 100644 --- a/tests/unit/Validator/StructureTest.php +++ b/tests/unit/Validator/StructureTest.php @@ -1074,6 +1074,25 @@ public function testTextValidation(): void '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); + // 'text_field' is optional (required => false). Structure short-circuits + // on `$required === false && is_null($value)` and `continue`s before + // building or running any validators, so the null never reaches the + // ByteLength check (which would otherwise reject it, since + // is_string(null) is false). Hence a null optional text value passes. + $this->assertEquals(true, $validator->isValid(new Document([ + '$collection' => ID::custom('posts'), + 'title' => 'Demo Title', + 'description' => 'Demo description', + 'rating' => 5, + 'price' => 1.99, + 'published' => true, + 'tags' => ['dog', 'cat', 'mouse'], + 'feedback' => 'team@appwrite.io', + 'text_field' => null, + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00' + ]))); + $this->assertEquals(false, $validator->isValid(new Document([ '$collection' => ID::custom('posts'), 'title' => 'Demo Title', From b5a2164c9f1a014e8389f92dd4d3a1864803e1cb Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 18 Jun 2026 12:47:51 +0300 Subject: [PATCH 12/18] Revert --- phpunit.xml | 2 +- src/Database/Validator/ByteLength.php | 2 +- tests/e2e/Adapter/Base.php | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/phpunit.xml b/phpunit.xml index 34365d48d..2a0531cfd 100755 --- a/phpunit.xml +++ b/phpunit.xml @@ -7,7 +7,7 @@ convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" - stopOnFailure="true" + stopOnFailure="false" > diff --git a/src/Database/Validator/ByteLength.php b/src/Database/Validator/ByteLength.php index b26d10a96..337dba74c 100644 --- a/src/Database/Validator/ByteLength.php +++ b/src/Database/Validator/ByteLength.php @@ -50,4 +50,4 @@ public function isValid(mixed $value): bool return true; } -} \ No newline at end of file +} diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index fee253d35..4baeba35b 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -23,8 +23,8 @@ abstract class Base extends TestCase { -// use CollectionTests; -// use CustomDocumentTypeTests; + use CollectionTests; + use CustomDocumentTypeTests; use DocumentTests; use AttributeTests; use IndexTests; From 93ef31c32b2e7ccb1d58c6896df2db4fef26c51f Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 18 Jun 2026 17:58:46 +0300 Subject: [PATCH 13/18] pull main --- tests/e2e/Adapter/Scopes/DocumentTests.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/e2e/Adapter/Scopes/DocumentTests.php b/tests/e2e/Adapter/Scopes/DocumentTests.php index b3fc61e48..0c2403ee6 100644 --- a/tests/e2e/Adapter/Scopes/DocumentTests.php +++ b/tests/e2e/Adapter/Scopes/DocumentTests.php @@ -976,6 +976,13 @@ public function testTextByteTruncationCreate(): void /** @var Database $database */ $database = $this->getDatabase(); + // Byte-capacity validation relies on attribute metadata, which + // schemaless adapters don't store, so there is nothing to enforce. + if (!$database->getAdapter()->getSupportForAttributes()) { + $this->expectNotToPerformAssertions(); + return; + } + $database->createCollection(__FUNCTION__); // A `text` attribute at its maximum allowed size. On MySQL/MariaDB this @@ -1015,6 +1022,13 @@ public function testTextByteTruncationValid(): void /** @var Database $database */ $database = $this->getDatabase(); + // Byte-capacity validation relies on attribute metadata, which + // schemaless adapters don't store, so there is nothing to enforce. + if (!$database->getAdapter()->getSupportForAttributes()) { + $this->expectNotToPerformAssertions(); + return; + } + $database->createCollection(__FUNCTION__); $database->createAttribute(__FUNCTION__, 'text', Database::VAR_TEXT, 65535, false); @@ -1045,6 +1059,13 @@ public function testTextByteTruncationUpdate(): void /** @var Database $database */ $database = $this->getDatabase(); + // Byte-capacity validation relies on attribute metadata, which + // schemaless adapters don't store, so there is nothing to enforce. + if (!$database->getAdapter()->getSupportForAttributes()) { + $this->expectNotToPerformAssertions(); + return; + } + $database->createCollection(__FUNCTION__); $database->createAttribute(__FUNCTION__, 'text', Database::VAR_TEXT, 65535, false); From 05e58dde6f52db6584f80cd8f0ff6037463214fb Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 18 Jun 2026 18:02:51 +0300 Subject: [PATCH 14/18] messages --- tests/e2e/Adapter/Scopes/DocumentTests.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/e2e/Adapter/Scopes/DocumentTests.php b/tests/e2e/Adapter/Scopes/DocumentTests.php index 0c2403ee6..fc10463a6 100644 --- a/tests/e2e/Adapter/Scopes/DocumentTests.php +++ b/tests/e2e/Adapter/Scopes/DocumentTests.php @@ -1022,8 +1022,6 @@ public function testTextByteTruncationValid(): void /** @var Database $database */ $database = $this->getDatabase(); - // Byte-capacity validation relies on attribute metadata, which - // schemaless adapters don't store, so there is nothing to enforce. if (!$database->getAdapter()->getSupportForAttributes()) { $this->expectNotToPerformAssertions(); return; @@ -1059,8 +1057,6 @@ public function testTextByteTruncationUpdate(): void /** @var Database $database */ $database = $this->getDatabase(); - // Byte-capacity validation relies on attribute metadata, which - // schemaless adapters don't store, so there is nothing to enforce. if (!$database->getAdapter()->getSupportForAttributes()) { $this->expectNotToPerformAssertions(); return; From 5656ad48418cde3490d9fe77136a312ffbe8bd6f Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 21 Jun 2026 09:19:41 +0300 Subject: [PATCH 15/18] fix SQLite.php --- src/Database/Adapter/SQLite.php | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index 93b618220..df1416933 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -36,15 +36,6 @@ */ class SQLite extends MariaDB { - /** - * MariaDB byte ceilings for TEXT-family types, mirrored so PRAGMA-based - * introspection produces the same characterMaximumLength values that - * INFORMATION_SCHEMA.COLUMNS would on MariaDB. - */ - private const MARIADB_TEXT_BYTES = '' . Database::MAX_TEXT_BYTES; - private const MARIADB_MEDIUMTEXT_BYTES = '' . Database::MAX_MEDIUMTEXT_BYTES; - private const MARIADB_LONGTEXT_BYTES = '' . Database::MAX_LONGTEXT_BYTES; - /** Suffix appended to every FTS5 virtual table name created by this adapter. */ private const FTS_TABLE_SUFFIX = '_fts'; @@ -3007,19 +2998,25 @@ private function parseSqliteColumnType(string $declaration): array break; } + /** + * MariaDB byte ceilings for TEXT-family types, mirrored so PRAGMA-based + * introspection produces the same characterMaximumLength values that + * INFORMATION_SCHEMA.COLUMNS would on MariaDB. + */ + if ($this->emulateMySQL) { switch ($dataType) { case 'text': - $result['characterMaximumLength'] = self::MARIADB_TEXT_BYTES; + $result['characterMaximumLength'] = '' . Database::MAX_TEXT_BYTES; break; case 'mediumtext': - $result['characterMaximumLength'] = self::MARIADB_MEDIUMTEXT_BYTES; + $result['characterMaximumLength'] = '' . Database::MAX_MEDIUMTEXT_BYTES; break; case 'longtext': case 'json': - $result['characterMaximumLength'] = self::MARIADB_LONGTEXT_BYTES; + $result['characterMaximumLength'] = '' . Database::MAX_LONGTEXT_BYTES; break; case 'tinyint': From d81506e98c71054d5e10fd0411c017e6ef0058bf Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 21 Jun 2026 09:23:43 +0300 Subject: [PATCH 16/18] line --- src/Database/Adapter/SQLite.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index df1416933..e824ad58a 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -41,6 +41,7 @@ class SQLite extends MariaDB /** AFTER INSERT trigger suffix on the parent collection. */ private const FTS_TRIGGER_INSERT = 'ai'; + /** AFTER DELETE trigger suffix on the parent collection. */ private const FTS_TRIGGER_DELETE = 'ad'; From 29d2d430cf236f64c0da748fc0c293707c6afd22 Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 21 Jun 2026 09:39:17 +0300 Subject: [PATCH 17/18] ByteLength --- src/Database/Validator/Structure.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Database/Validator/Structure.php b/src/Database/Validator/Structure.php index 63565e72e..4e3f66ec4 100644 --- a/src/Database/Validator/Structure.php +++ b/src/Database/Validator/Structure.php @@ -354,17 +354,17 @@ protected function checkForInvalidAttributeValues(Document $document, array $str break; case Database::VAR_TEXT: - $validators[] = new Text($size, min: 0); + $validators[] = new ByteLength($size); $validators[] = new ByteLength(Database::MAX_TEXT_BYTES); break; case Database::VAR_MEDIUMTEXT: - $validators[] = new Text($size, min: 0); + $validators[] = new ByteLength($size); $validators[] = new ByteLength(Database::MAX_MEDIUMTEXT_BYTES); break; case Database::VAR_LONGTEXT: - $validators[] = new Text($size, min: 0); + $validators[] = new ByteLength($size); $validators[] = new ByteLength(Database::MAX_LONGTEXT_BYTES); break; From 8c7b1317ad3233fb4eeb0f22c4250606b07220ad Mon Sep 17 00:00:00 2001 From: fogelito Date: Sun, 21 Jun 2026 09:57:13 +0300 Subject: [PATCH 18/18] tests --- tests/unit/Validator/StructureTest.php | 59 ++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/tests/unit/Validator/StructureTest.php b/tests/unit/Validator/StructureTest.php index b7a6cbf69..71c90577c 100644 --- a/tests/unit/Validator/StructureTest.php +++ b/tests/unit/Validator/StructureTest.php @@ -1107,7 +1107,7 @@ public function testTextValidation(): void '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); - $this->assertEquals('Invalid document structure: Attribute "text_field" has invalid type. Value must be a valid string and no longer than 65535 chars', $validator->getDescription()); + $this->assertEquals('Invalid document structure: Attribute "text_field" has invalid type. Value must be a valid string no longer than 65535 bytes', $validator->getDescription()); $this->assertEquals(false, $validator->isValid(new Document([ '$collection' => ID::custom('posts'), @@ -1123,7 +1123,7 @@ public function testTextValidation(): void '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); - $this->assertEquals('Invalid document structure: Attribute "text_field" has invalid type. Value must be a valid string and no longer than 65535 chars', $validator->getDescription()); + $this->assertEquals('Invalid document structure: Attribute "text_field" has invalid type. Value must be a valid string no longer than 65535 bytes', $validator->getDescription()); } public function testMediumtextValidation(): void @@ -1161,7 +1161,58 @@ public function testMediumtextValidation(): void '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); - $this->assertEquals('Invalid document structure: Attribute "mediumtext_field" has invalid type. Value must be a valid string and no longer than 16777215 chars', $validator->getDescription()); + $this->assertEquals('Invalid document structure: Attribute "mediumtext_field" has invalid type. Value must be a valid string no longer than 16777215 bytes', $validator->getDescription()); + } + + public function testMediumtextSizedValidation(): void + { + // A mediumtext attribute with a declared size of 100. The declared size + // is enforced as a byte limit (ByteLength), independent of the column's + // physical 16MB ceiling. + $collection = new Document([ + '$id' => ID::custom('posts'), + '$collection' => Database::METADATA, + 'name' => 'posts', + 'attributes' => [ + [ + '$id' => 'mediumtext', + 'type' => Database::VAR_MEDIUMTEXT, + 'format' => '', + 'size' => 100, + 'required' => false, + 'signed' => true, + 'array' => false, + 'filters' => [], + ], + ], + 'indexes' => [], + ]); + + $validator = new Structure($collection, Database::VAR_INTEGER); + + $base = [ + '$collection' => ID::custom('posts'), + '$createdAt' => '2000-04-01T12:00:00.000+00:00', + '$updatedAt' => '2000-04-01T12:00:00.000+00:00', + ]; + + // A value of exactly 100 bytes fits the declared size. + $exact = \str_repeat('a', 100); + $this->assertEquals(true, $validator->isValid(new Document($base + ['mediumtext' => $exact]))); + + // 101 bytes exceeds the declared size of 100 bytes and is rejected. + $tooBig = \str_repeat('a', 101); + $this->assertEquals(false, $validator->isValid(new Document($base + ['mediumtext' => $tooBig]))); + $this->assertEquals('Invalid document structure: Attribute "mediumtext" has invalid type. Value must be a valid string no longer than 100 bytes', $validator->getDescription()); + + // Each '📝' is 4 bytes in utf8mb4, so 25 chars = 100 bytes fits exactly, + // while 26 chars = 104 bytes exceeds the declared 100-byte size. + $exactMultibyte = \str_repeat('📝', 25); + $this->assertEquals(true, $validator->isValid(new Document($base + ['mediumtext' => $exactMultibyte]))); + + $tooBigMultibyte = \str_repeat('📝', 26); + $this->assertEquals(false, $validator->isValid(new Document($base + ['mediumtext' => $tooBigMultibyte]))); + $this->assertEquals('Invalid document structure: Attribute "mediumtext" has invalid type. Value must be a valid string no longer than 100 bytes', $validator->getDescription()); } public function testLongtextValidation(): void @@ -1199,7 +1250,7 @@ public function testLongtextValidation(): void '$updatedAt' => '2000-04-01T12:00:00.000+00:00' ]))); - $this->assertEquals('Invalid document structure: Attribute "longtext_field" has invalid type. Value must be a valid string and no longer than 4294967295 chars', $validator->getDescription()); + $this->assertEquals('Invalid document structure: Attribute "longtext_field" has invalid type. Value must be a valid string no longer than 4294967295 bytes', $validator->getDescription()); } public function testStringTypeArrayValidation(): void