From 5a835043415e90842f22668dad8a935d0eb07cd4 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 17 Nov 2025 18:37:11 +0530 Subject: [PATCH 1/4] added hashing for the key in postgres if more than 63 --- src/Database/Adapter/Postgres.php | 87 ++++++++++++++---- tests/e2e/Adapter/Scopes/CollectionTests.php | 97 ++++++++++++++++++++ 2 files changed, 166 insertions(+), 18 deletions(-) diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 72e49cc07..aa1127f9b 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -30,6 +30,7 @@ */ class Postgres extends SQL { + const MAX_IDENTIFIER_NAME = 63; /** * @inheritDoc */ @@ -244,17 +245,24 @@ public function createCollection(string $name, array $attributes = [], array $in "; if ($this->sharedTables) { + $uidIndex = $this->getIndexKey("{$namespace}_{$this->tenant}_{$id}_uid"); + $createdIndex = $this->getIndexKey("{$namespace}_{$this->tenant}_{$id}_created"); + $updatedIndex = $this->getIndexKey("{$namespace}_{$this->tenant}_{$id}_updated"); + $tenantIdIndex = $this->getIndexKey("{$namespace}_{$this->tenant}_{$id}_tenant_id"); $collection .= " - CREATE UNIQUE INDEX \"{$namespace}_{$this->tenant}_{$id}_uid\" ON {$this->getSQLTable($id)} (\"_uid\" COLLATE utf8_ci_ai, \"_tenant\"); - CREATE INDEX \"{$namespace}_{$this->tenant}_{$id}_created\" ON {$this->getSQLTable($id)} (_tenant, \"_createdAt\"); - CREATE INDEX \"{$namespace}_{$this->tenant}_{$id}_updated\" ON {$this->getSQLTable($id)} (_tenant, \"_updatedAt\"); - CREATE INDEX \"{$namespace}_{$this->tenant}_{$id}_tenant_id\" ON {$this->getSQLTable($id)} (_tenant, _id); + CREATE UNIQUE INDEX \"{$uidIndex}\" ON {$this->getSQLTable($id)} (\"_uid\" COLLATE utf8_ci_ai, \"_tenant\"); + CREATE INDEX \"{$createdIndex}\" ON {$this->getSQLTable($id)} (_tenant, \"_createdAt\"); + CREATE INDEX \"{$updatedIndex}\" ON {$this->getSQLTable($id)} (_tenant, \"_updatedAt\"); + CREATE INDEX \"{$tenantIdIndex}\" ON {$this->getSQLTable($id)} (_tenant, _id); "; } else { + $uidIndex = $this->getIndexKey("{$namespace}_{$id}_uid"); + $createdIndex = $this->getIndexKey("{$namespace}_{$id}_created"); + $updatedIndex = $this->getIndexKey("{$namespace}_{$id}_updated"); $collection .= " - CREATE UNIQUE INDEX \"{$namespace}_{$id}_uid\" ON {$this->getSQLTable($id)} (\"_uid\" COLLATE utf8_ci_ai); - CREATE INDEX \"{$namespace}_{$id}_created\" ON {$this->getSQLTable($id)} (\"_createdAt\"); - CREATE INDEX \"{$namespace}_{$id}_updated\" ON {$this->getSQLTable($id)} (\"_updatedAt\"); + CREATE UNIQUE INDEX \"{$uidIndex}\" ON {$this->getSQLTable($id)} (\"_uid\" COLLATE utf8_ci_ai); + CREATE INDEX \"{$createdIndex}\" ON {$this->getSQLTable($id)} (\"_createdAt\"); + CREATE INDEX \"{$updatedIndex}\" ON {$this->getSQLTable($id)} (\"_updatedAt\"); "; } @@ -271,17 +279,21 @@ public function createCollection(string $name, array $attributes = [], array $in "; if ($this->sharedTables) { + $uniquePermissionIndex = $this->getIndexKey("{$namespace}_{$this->tenant}_{$id}_ukey"); + $permissionIndex = $this->getIndexKey("{$namespace}_{$this->tenant}_{$id}_permission"); $permissions .= " - CREATE UNIQUE INDEX \"{$namespace}_{$this->tenant}_{$id}_ukey\" + CREATE UNIQUE INDEX \"{$uniquePermissionIndex}\" ON {$this->getSQLTable($id . '_perms')} USING btree (_tenant,_document,_type,_permission); - CREATE INDEX \"{$namespace}_{$this->tenant}_{$id}_permission\" + CREATE INDEX \"{$permissionIndex}\" ON {$this->getSQLTable($id . '_perms')} USING btree (_tenant,_permission,_type); "; } else { + $uniquePermissionIndex = $this->getIndexKey("{$namespace}_{$id}_ukey"); + $permissionIndex = $this->getIndexKey("{$namespace}_{$id}_permission"); $permissions .= " - CREATE UNIQUE INDEX \"{$namespace}_{$id}_ukey\" + CREATE UNIQUE INDEX \"{$uniquePermissionIndex}\" ON {$this->getSQLTable($id . '_perms')} USING btree (_document COLLATE utf8_ci_ai,_type,_permission); - CREATE INDEX \"{$namespace}_{$id}_permission\" + CREATE INDEX \"{$permissionIndex}\" ON {$this->getSQLTable($id . '_perms')} USING btree (_permission,_type); "; } @@ -893,7 +905,7 @@ public function createIndex(string $collection, string $id, string $type, array default => throw new DatabaseException('Unknown index type: ' . $type . '. Must be one of ' . Database::INDEX_KEY . ', ' . Database::INDEX_UNIQUE . ', ' . Database::INDEX_FULLTEXT . ', ' . Database::INDEX_SPATIAL . ', ' . Database::INDEX_OBJECT . ', ' . Database::INDEX_HNSW_EUCLIDEAN . ', ' . Database::INDEX_HNSW_COSINE . ', ' . Database::INDEX_HNSW_DOT), }; - $key = "\"{$this->getNamespace()}_{$this->tenant}_{$collection}_{$id}\""; + $keyName = $this->getIndexKey("{$this->getNamespace()}_{$this->tenant}_{$collection}_{$id}"); $attributes = \implode(', ', $attributes); if ($this->sharedTables && \in_array($type, [Database::INDEX_KEY, Database::INDEX_UNIQUE])) { @@ -901,7 +913,7 @@ public function createIndex(string $collection, string $id, string $type, array $attributes = "_tenant, {$attributes}"; } - $sql = "CREATE {$sqlType} {$key} ON {$this->getSQLTable($collection)}"; + $sql = "CREATE {$sqlType} \"{$keyName}\" ON {$this->getSQLTable($collection)}"; // Add USING clause for special index types $sql .= match ($type) { @@ -936,9 +948,9 @@ public function deleteIndex(string $collection, string $id): bool $id = $this->filter($id); $schemaName = $this->getDatabase(); - $key = "\"{$this->getNamespace()}_{$this->tenant}_{$collection}_{$id}\""; + $keyName = $this->getIndexKey("{$this->getNamespace()}_{$this->tenant}_{$collection}_{$id}"); - $sql = "DROP INDEX IF EXISTS \"{$schemaName}\".{$key}"; + $sql = "DROP INDEX IF EXISTS \"{$schemaName}\".\"{$keyName}\""; $sql = $this->trigger(Database::EVENT_INDEX_DELETE, $sql); return $this->execute($this->getPDO() @@ -961,10 +973,11 @@ public function renameIndex(string $collection, string $old, string $new): bool $namespace = $this->getNamespace(); $old = $this->filter($old); $new = $this->filter($new); - $oldIndexName = "{$this->tenant}_{$collection}_{$old}"; - $newIndexName = "{$namespace}_{$this->tenant}_{$collection}_{$new}"; + $schema = $this->getDatabase(); + $oldIndexName = $this->getIndexKey("{$namespace}_{$this->tenant}_{$collection}_{$old}"); + $newIndexName = $this->getIndexKey("{$namespace}_{$this->tenant}_{$collection}_{$new}"); - $sql = "ALTER INDEX {$this->getSQLTable($oldIndexName)} RENAME TO \"{$newIndexName}\""; + $sql = "ALTER INDEX \"{$schema}\".\"{$oldIndexName}\" RENAME TO \"{$newIndexName}\""; $sql = $this->trigger(Database::EVENT_INDEX_RENAME, $sql); return $this->execute($this->getPDO() @@ -2738,4 +2751,42 @@ protected function bindOperatorParams(\PDOStatement|PDOStatementProxy $stmt, Ope break; } } + + /** + * Ensure index key length stays within PostgreSQL's 63 character limit. + * + * @param string $key + * @return string + */ + protected function getIndexKey(string $key): string + { + if (\strlen($key) <= self::MAX_IDENTIFIER_NAME) { + return $key; + } + + $suffix = ''; + $separatorPosition = strrpos($key, '_'); + if ($separatorPosition !== false) { + $suffix = substr($key, $separatorPosition + 1); + } + + $hash = hash('crc32b', $key); + + if ($suffix !== '') { + $hashedKey = "{$hash}_{$suffix}"; + if (\strlen($hashedKey) <= self::MAX_IDENTIFIER_NAME) { + return $hashedKey; + } + } + + return substr($hash, 0, self::MAX_IDENTIFIER_NAME); + } + + protected function getSQLTable(string $name): string + { + $table = "{$this->getNamespace()}_{$this->filter($name)}"; + $table = $this->getIndexKey($table); + + return "{$this->quote($this->getDatabase())}.{$this->quote($table)}"; + } } diff --git a/tests/e2e/Adapter/Scopes/CollectionTests.php b/tests/e2e/Adapter/Scopes/CollectionTests.php index 2f94ff09c..f6212fe44 100644 --- a/tests/e2e/Adapter/Scopes/CollectionTests.php +++ b/tests/e2e/Adapter/Scopes/CollectionTests.php @@ -1582,4 +1582,101 @@ public function testSetGlobalCollection(): void $this->assertEmpty($db->getGlobalCollections()); } + + public function testCreateCollectionWithLongId(): void + { + $database = static::getDatabase(); + + if (!$database->getAdapter()->getSupportForAttributes()) { + $this->expectNotToPerformAssertions(); + return; + } + + $collection = '019a91aa-58cd-708d-a55c-5f7725ef937a'; + + $attributes = [ + new Document([ + '$id' => 'name', + 'type' => Database::VAR_STRING, + 'size' => 256, + 'required' => true, + 'array' => false, + ]), + new Document([ + '$id' => 'age', + 'type' => Database::VAR_INTEGER, + 'size' => 0, + 'required' => false, + 'array' => false, + ]), + new Document([ + '$id' => 'isActive', + 'type' => Database::VAR_BOOLEAN, + 'size' => 0, + 'required' => false, + 'array' => false, + ]), + ]; + + $indexes = [ + new Document([ + '$id' => ID::custom('idx_name'), + 'type' => Database::INDEX_KEY, + 'attributes' => ['name'], + 'lengths' => [128], + 'orders' => ['ASC'], + ]), + new Document([ + '$id' => ID::custom('idx_name_age'), + 'type' => Database::INDEX_KEY, + 'attributes' => ['name', 'age'], + 'lengths' => [128, null], + 'orders' => ['ASC', 'DESC'], + ]), + ]; + + $collectionDocument = $database->createCollection( + $collection, + $attributes, + $indexes, + permissions: [ + Permission::read(Role::any()), + Permission::create(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ); + + $this->assertEquals($collection, $collectionDocument->getId()); + $this->assertCount(3, $collectionDocument->getAttribute('attributes')); + $this->assertCount(2, $collectionDocument->getAttribute('indexes')); + + $document = $database->createDocument($collection, new Document([ + '$id' => 'longIdDoc', + '$permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + ], + 'name' => 'LongId Test', + 'age' => 42, + 'isActive' => true, + ])); + + $this->assertEquals('longIdDoc', $document->getId()); + $this->assertEquals('LongId Test', $document->getAttribute('name')); + $this->assertEquals(42, $document->getAttribute('age')); + $this->assertTrue($document->getAttribute('isActive')); + + $found = $database->find($collection, [ + Query::equal('name', ['LongId Test']), + ]); + + $this->assertCount(1, $found); + $this->assertEquals('longIdDoc', $found[0]->getId()); + + $fetched = $database->getDocument($collection, 'longIdDoc'); + $this->assertEquals('LongId Test', $fetched->getAttribute('name')); + + $this->assertTrue($database->deleteCollection($collection)); + } } From 1ee6436314222b9523362bbfbc26cbf7ccfe817c Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 17 Nov 2025 18:41:50 +0530 Subject: [PATCH 2/4] linting --- src/Database/Adapter/Postgres.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index aa1127f9b..1b2689ad6 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -30,7 +30,7 @@ */ class Postgres extends SQL { - const MAX_IDENTIFIER_NAME = 63; + public const MAX_IDENTIFIER_NAME = 63; /** * @inheritDoc */ From 68d47bf64e8e0f98c4f9cfe37050f81a81130422 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Tue, 18 Nov 2025 10:12:01 +0530 Subject: [PATCH 3/4] changed hashing to use md5 instead of crc for collision issues --- src/Database/Adapter/Postgres.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 1b2689ad6..7497fe693 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -2770,7 +2770,7 @@ protected function getIndexKey(string $key): string $suffix = substr($key, $separatorPosition + 1); } - $hash = hash('crc32b', $key); + $hash = md5($key); if ($suffix !== '') { $hashedKey = "{$hash}_{$suffix}"; From ef4429ec8027be61aa25b7d24b7c85e775a280d8 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Tue, 18 Nov 2025 12:02:20 +0530 Subject: [PATCH 4/4] renamed getIndexKey to getShortKey --- src/Database/Adapter/Postgres.php | 34 +++++++++++++++---------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index 7497fe693..86da09a58 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -245,10 +245,10 @@ public function createCollection(string $name, array $attributes = [], array $in "; if ($this->sharedTables) { - $uidIndex = $this->getIndexKey("{$namespace}_{$this->tenant}_{$id}_uid"); - $createdIndex = $this->getIndexKey("{$namespace}_{$this->tenant}_{$id}_created"); - $updatedIndex = $this->getIndexKey("{$namespace}_{$this->tenant}_{$id}_updated"); - $tenantIdIndex = $this->getIndexKey("{$namespace}_{$this->tenant}_{$id}_tenant_id"); + $uidIndex = $this->getShortKey("{$namespace}_{$this->tenant}_{$id}_uid"); + $createdIndex = $this->getShortKey("{$namespace}_{$this->tenant}_{$id}_created"); + $updatedIndex = $this->getShortKey("{$namespace}_{$this->tenant}_{$id}_updated"); + $tenantIdIndex = $this->getShortKey("{$namespace}_{$this->tenant}_{$id}_tenant_id"); $collection .= " CREATE UNIQUE INDEX \"{$uidIndex}\" ON {$this->getSQLTable($id)} (\"_uid\" COLLATE utf8_ci_ai, \"_tenant\"); CREATE INDEX \"{$createdIndex}\" ON {$this->getSQLTable($id)} (_tenant, \"_createdAt\"); @@ -256,9 +256,9 @@ public function createCollection(string $name, array $attributes = [], array $in CREATE INDEX \"{$tenantIdIndex}\" ON {$this->getSQLTable($id)} (_tenant, _id); "; } else { - $uidIndex = $this->getIndexKey("{$namespace}_{$id}_uid"); - $createdIndex = $this->getIndexKey("{$namespace}_{$id}_created"); - $updatedIndex = $this->getIndexKey("{$namespace}_{$id}_updated"); + $uidIndex = $this->getShortKey("{$namespace}_{$id}_uid"); + $createdIndex = $this->getShortKey("{$namespace}_{$id}_created"); + $updatedIndex = $this->getShortKey("{$namespace}_{$id}_updated"); $collection .= " CREATE UNIQUE INDEX \"{$uidIndex}\" ON {$this->getSQLTable($id)} (\"_uid\" COLLATE utf8_ci_ai); CREATE INDEX \"{$createdIndex}\" ON {$this->getSQLTable($id)} (\"_createdAt\"); @@ -279,8 +279,8 @@ public function createCollection(string $name, array $attributes = [], array $in "; if ($this->sharedTables) { - $uniquePermissionIndex = $this->getIndexKey("{$namespace}_{$this->tenant}_{$id}_ukey"); - $permissionIndex = $this->getIndexKey("{$namespace}_{$this->tenant}_{$id}_permission"); + $uniquePermissionIndex = $this->getShortKey("{$namespace}_{$this->tenant}_{$id}_ukey"); + $permissionIndex = $this->getShortKey("{$namespace}_{$this->tenant}_{$id}_permission"); $permissions .= " CREATE UNIQUE INDEX \"{$uniquePermissionIndex}\" ON {$this->getSQLTable($id . '_perms')} USING btree (_tenant,_document,_type,_permission); @@ -288,8 +288,8 @@ public function createCollection(string $name, array $attributes = [], array $in ON {$this->getSQLTable($id . '_perms')} USING btree (_tenant,_permission,_type); "; } else { - $uniquePermissionIndex = $this->getIndexKey("{$namespace}_{$id}_ukey"); - $permissionIndex = $this->getIndexKey("{$namespace}_{$id}_permission"); + $uniquePermissionIndex = $this->getShortKey("{$namespace}_{$id}_ukey"); + $permissionIndex = $this->getShortKey("{$namespace}_{$id}_permission"); $permissions .= " CREATE UNIQUE INDEX \"{$uniquePermissionIndex}\" ON {$this->getSQLTable($id . '_perms')} USING btree (_document COLLATE utf8_ci_ai,_type,_permission); @@ -905,7 +905,7 @@ public function createIndex(string $collection, string $id, string $type, array default => throw new DatabaseException('Unknown index type: ' . $type . '. Must be one of ' . Database::INDEX_KEY . ', ' . Database::INDEX_UNIQUE . ', ' . Database::INDEX_FULLTEXT . ', ' . Database::INDEX_SPATIAL . ', ' . Database::INDEX_OBJECT . ', ' . Database::INDEX_HNSW_EUCLIDEAN . ', ' . Database::INDEX_HNSW_COSINE . ', ' . Database::INDEX_HNSW_DOT), }; - $keyName = $this->getIndexKey("{$this->getNamespace()}_{$this->tenant}_{$collection}_{$id}"); + $keyName = $this->getShortKey("{$this->getNamespace()}_{$this->tenant}_{$collection}_{$id}"); $attributes = \implode(', ', $attributes); if ($this->sharedTables && \in_array($type, [Database::INDEX_KEY, Database::INDEX_UNIQUE])) { @@ -948,7 +948,7 @@ public function deleteIndex(string $collection, string $id): bool $id = $this->filter($id); $schemaName = $this->getDatabase(); - $keyName = $this->getIndexKey("{$this->getNamespace()}_{$this->tenant}_{$collection}_{$id}"); + $keyName = $this->getShortKey("{$this->getNamespace()}_{$this->tenant}_{$collection}_{$id}"); $sql = "DROP INDEX IF EXISTS \"{$schemaName}\".\"{$keyName}\""; $sql = $this->trigger(Database::EVENT_INDEX_DELETE, $sql); @@ -974,8 +974,8 @@ public function renameIndex(string $collection, string $old, string $new): bool $old = $this->filter($old); $new = $this->filter($new); $schema = $this->getDatabase(); - $oldIndexName = $this->getIndexKey("{$namespace}_{$this->tenant}_{$collection}_{$old}"); - $newIndexName = $this->getIndexKey("{$namespace}_{$this->tenant}_{$collection}_{$new}"); + $oldIndexName = $this->getShortKey("{$namespace}_{$this->tenant}_{$collection}_{$old}"); + $newIndexName = $this->getShortKey("{$namespace}_{$this->tenant}_{$collection}_{$new}"); $sql = "ALTER INDEX \"{$schema}\".\"{$oldIndexName}\" RENAME TO \"{$newIndexName}\""; $sql = $this->trigger(Database::EVENT_INDEX_RENAME, $sql); @@ -2758,7 +2758,7 @@ protected function bindOperatorParams(\PDOStatement|PDOStatementProxy $stmt, Ope * @param string $key * @return string */ - protected function getIndexKey(string $key): string + protected function getShortKey(string $key): string { if (\strlen($key) <= self::MAX_IDENTIFIER_NAME) { return $key; @@ -2785,7 +2785,7 @@ protected function getIndexKey(string $key): string protected function getSQLTable(string $name): string { $table = "{$this->getNamespace()}_{$this->filter($name)}"; - $table = $this->getIndexKey($table); + $table = $this->getShortKey($table); return "{$this->quote($this->getDatabase())}.{$this->quote($table)}"; }