Skip to content

Commit 5da71b6

Browse files
authored
Merge pull request #761 from utopia-php/dat-923
Added hashing for the key in postgres if more than 63
2 parents e10b4fa + ef4429e commit 5da71b6

File tree

2 files changed

+166
-18
lines changed

2 files changed

+166
-18
lines changed

src/Database/Adapter/Postgres.php

Lines changed: 69 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
*/
3131
class Postgres extends SQL
3232
{
33+
public const MAX_IDENTIFIER_NAME = 63;
3334
/**
3435
* @inheritDoc
3536
*/
@@ -244,17 +245,24 @@ public function createCollection(string $name, array $attributes = [], array $in
244245
";
245246

246247
if ($this->sharedTables) {
248+
$uidIndex = $this->getShortKey("{$namespace}_{$this->tenant}_{$id}_uid");
249+
$createdIndex = $this->getShortKey("{$namespace}_{$this->tenant}_{$id}_created");
250+
$updatedIndex = $this->getShortKey("{$namespace}_{$this->tenant}_{$id}_updated");
251+
$tenantIdIndex = $this->getShortKey("{$namespace}_{$this->tenant}_{$id}_tenant_id");
247252
$collection .= "
248-
CREATE UNIQUE INDEX \"{$namespace}_{$this->tenant}_{$id}_uid\" ON {$this->getSQLTable($id)} (\"_uid\" COLLATE utf8_ci_ai, \"_tenant\");
249-
CREATE INDEX \"{$namespace}_{$this->tenant}_{$id}_created\" ON {$this->getSQLTable($id)} (_tenant, \"_createdAt\");
250-
CREATE INDEX \"{$namespace}_{$this->tenant}_{$id}_updated\" ON {$this->getSQLTable($id)} (_tenant, \"_updatedAt\");
251-
CREATE INDEX \"{$namespace}_{$this->tenant}_{$id}_tenant_id\" ON {$this->getSQLTable($id)} (_tenant, _id);
253+
CREATE UNIQUE INDEX \"{$uidIndex}\" ON {$this->getSQLTable($id)} (\"_uid\" COLLATE utf8_ci_ai, \"_tenant\");
254+
CREATE INDEX \"{$createdIndex}\" ON {$this->getSQLTable($id)} (_tenant, \"_createdAt\");
255+
CREATE INDEX \"{$updatedIndex}\" ON {$this->getSQLTable($id)} (_tenant, \"_updatedAt\");
256+
CREATE INDEX \"{$tenantIdIndex}\" ON {$this->getSQLTable($id)} (_tenant, _id);
252257
";
253258
} else {
259+
$uidIndex = $this->getShortKey("{$namespace}_{$id}_uid");
260+
$createdIndex = $this->getShortKey("{$namespace}_{$id}_created");
261+
$updatedIndex = $this->getShortKey("{$namespace}_{$id}_updated");
254262
$collection .= "
255-
CREATE UNIQUE INDEX \"{$namespace}_{$id}_uid\" ON {$this->getSQLTable($id)} (\"_uid\" COLLATE utf8_ci_ai);
256-
CREATE INDEX \"{$namespace}_{$id}_created\" ON {$this->getSQLTable($id)} (\"_createdAt\");
257-
CREATE INDEX \"{$namespace}_{$id}_updated\" ON {$this->getSQLTable($id)} (\"_updatedAt\");
263+
CREATE UNIQUE INDEX \"{$uidIndex}\" ON {$this->getSQLTable($id)} (\"_uid\" COLLATE utf8_ci_ai);
264+
CREATE INDEX \"{$createdIndex}\" ON {$this->getSQLTable($id)} (\"_createdAt\");
265+
CREATE INDEX \"{$updatedIndex}\" ON {$this->getSQLTable($id)} (\"_updatedAt\");
258266
";
259267
}
260268

@@ -271,17 +279,21 @@ public function createCollection(string $name, array $attributes = [], array $in
271279
";
272280

273281
if ($this->sharedTables) {
282+
$uniquePermissionIndex = $this->getShortKey("{$namespace}_{$this->tenant}_{$id}_ukey");
283+
$permissionIndex = $this->getShortKey("{$namespace}_{$this->tenant}_{$id}_permission");
274284
$permissions .= "
275-
CREATE UNIQUE INDEX \"{$namespace}_{$this->tenant}_{$id}_ukey\"
285+
CREATE UNIQUE INDEX \"{$uniquePermissionIndex}\"
276286
ON {$this->getSQLTable($id . '_perms')} USING btree (_tenant,_document,_type,_permission);
277-
CREATE INDEX \"{$namespace}_{$this->tenant}_{$id}_permission\"
287+
CREATE INDEX \"{$permissionIndex}\"
278288
ON {$this->getSQLTable($id . '_perms')} USING btree (_tenant,_permission,_type);
279289
";
280290
} else {
291+
$uniquePermissionIndex = $this->getShortKey("{$namespace}_{$id}_ukey");
292+
$permissionIndex = $this->getShortKey("{$namespace}_{$id}_permission");
281293
$permissions .= "
282-
CREATE UNIQUE INDEX \"{$namespace}_{$id}_ukey\"
294+
CREATE UNIQUE INDEX \"{$uniquePermissionIndex}\"
283295
ON {$this->getSQLTable($id . '_perms')} USING btree (_document COLLATE utf8_ci_ai,_type,_permission);
284-
CREATE INDEX \"{$namespace}_{$id}_permission\"
296+
CREATE INDEX \"{$permissionIndex}\"
285297
ON {$this->getSQLTable($id . '_perms')} USING btree (_permission,_type);
286298
";
287299
}
@@ -893,15 +905,15 @@ public function createIndex(string $collection, string $id, string $type, array
893905
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),
894906
};
895907

896-
$key = "\"{$this->getNamespace()}_{$this->tenant}_{$collection}_{$id}\"";
908+
$keyName = $this->getShortKey("{$this->getNamespace()}_{$this->tenant}_{$collection}_{$id}");
897909
$attributes = \implode(', ', $attributes);
898910

899911
if ($this->sharedTables && \in_array($type, [Database::INDEX_KEY, Database::INDEX_UNIQUE])) {
900912
// Add tenant as first index column for best performance
901913
$attributes = "_tenant, {$attributes}";
902914
}
903915

904-
$sql = "CREATE {$sqlType} {$key} ON {$this->getSQLTable($collection)}";
916+
$sql = "CREATE {$sqlType} \"{$keyName}\" ON {$this->getSQLTable($collection)}";
905917

906918
// Add USING clause for special index types
907919
$sql .= match ($type) {
@@ -936,9 +948,9 @@ public function deleteIndex(string $collection, string $id): bool
936948
$id = $this->filter($id);
937949
$schemaName = $this->getDatabase();
938950

939-
$key = "\"{$this->getNamespace()}_{$this->tenant}_{$collection}_{$id}\"";
951+
$keyName = $this->getShortKey("{$this->getNamespace()}_{$this->tenant}_{$collection}_{$id}");
940952

941-
$sql = "DROP INDEX IF EXISTS \"{$schemaName}\".{$key}";
953+
$sql = "DROP INDEX IF EXISTS \"{$schemaName}\".\"{$keyName}\"";
942954
$sql = $this->trigger(Database::EVENT_INDEX_DELETE, $sql);
943955

944956
return $this->execute($this->getPDO()
@@ -961,10 +973,11 @@ public function renameIndex(string $collection, string $old, string $new): bool
961973
$namespace = $this->getNamespace();
962974
$old = $this->filter($old);
963975
$new = $this->filter($new);
964-
$oldIndexName = "{$this->tenant}_{$collection}_{$old}";
965-
$newIndexName = "{$namespace}_{$this->tenant}_{$collection}_{$new}";
976+
$schema = $this->getDatabase();
977+
$oldIndexName = $this->getShortKey("{$namespace}_{$this->tenant}_{$collection}_{$old}");
978+
$newIndexName = $this->getShortKey("{$namespace}_{$this->tenant}_{$collection}_{$new}");
966979

967-
$sql = "ALTER INDEX {$this->getSQLTable($oldIndexName)} RENAME TO \"{$newIndexName}\"";
980+
$sql = "ALTER INDEX \"{$schema}\".\"{$oldIndexName}\" RENAME TO \"{$newIndexName}\"";
968981
$sql = $this->trigger(Database::EVENT_INDEX_RENAME, $sql);
969982

970983
return $this->execute($this->getPDO()
@@ -2738,4 +2751,42 @@ protected function bindOperatorParams(\PDOStatement|PDOStatementProxy $stmt, Ope
27382751
break;
27392752
}
27402753
}
2754+
2755+
/**
2756+
* Ensure index key length stays within PostgreSQL's 63 character limit.
2757+
*
2758+
* @param string $key
2759+
* @return string
2760+
*/
2761+
protected function getShortKey(string $key): string
2762+
{
2763+
if (\strlen($key) <= self::MAX_IDENTIFIER_NAME) {
2764+
return $key;
2765+
}
2766+
2767+
$suffix = '';
2768+
$separatorPosition = strrpos($key, '_');
2769+
if ($separatorPosition !== false) {
2770+
$suffix = substr($key, $separatorPosition + 1);
2771+
}
2772+
2773+
$hash = md5($key);
2774+
2775+
if ($suffix !== '') {
2776+
$hashedKey = "{$hash}_{$suffix}";
2777+
if (\strlen($hashedKey) <= self::MAX_IDENTIFIER_NAME) {
2778+
return $hashedKey;
2779+
}
2780+
}
2781+
2782+
return substr($hash, 0, self::MAX_IDENTIFIER_NAME);
2783+
}
2784+
2785+
protected function getSQLTable(string $name): string
2786+
{
2787+
$table = "{$this->getNamespace()}_{$this->filter($name)}";
2788+
$table = $this->getShortKey($table);
2789+
2790+
return "{$this->quote($this->getDatabase())}.{$this->quote($table)}";
2791+
}
27412792
}

tests/e2e/Adapter/Scopes/CollectionTests.php

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1582,4 +1582,101 @@ public function testSetGlobalCollection(): void
15821582
$this->assertEmpty($db->getGlobalCollections());
15831583

15841584
}
1585+
1586+
public function testCreateCollectionWithLongId(): void
1587+
{
1588+
$database = static::getDatabase();
1589+
1590+
if (!$database->getAdapter()->getSupportForAttributes()) {
1591+
$this->expectNotToPerformAssertions();
1592+
return;
1593+
}
1594+
1595+
$collection = '019a91aa-58cd-708d-a55c-5f7725ef937a';
1596+
1597+
$attributes = [
1598+
new Document([
1599+
'$id' => 'name',
1600+
'type' => Database::VAR_STRING,
1601+
'size' => 256,
1602+
'required' => true,
1603+
'array' => false,
1604+
]),
1605+
new Document([
1606+
'$id' => 'age',
1607+
'type' => Database::VAR_INTEGER,
1608+
'size' => 0,
1609+
'required' => false,
1610+
'array' => false,
1611+
]),
1612+
new Document([
1613+
'$id' => 'isActive',
1614+
'type' => Database::VAR_BOOLEAN,
1615+
'size' => 0,
1616+
'required' => false,
1617+
'array' => false,
1618+
]),
1619+
];
1620+
1621+
$indexes = [
1622+
new Document([
1623+
'$id' => ID::custom('idx_name'),
1624+
'type' => Database::INDEX_KEY,
1625+
'attributes' => ['name'],
1626+
'lengths' => [128],
1627+
'orders' => ['ASC'],
1628+
]),
1629+
new Document([
1630+
'$id' => ID::custom('idx_name_age'),
1631+
'type' => Database::INDEX_KEY,
1632+
'attributes' => ['name', 'age'],
1633+
'lengths' => [128, null],
1634+
'orders' => ['ASC', 'DESC'],
1635+
]),
1636+
];
1637+
1638+
$collectionDocument = $database->createCollection(
1639+
$collection,
1640+
$attributes,
1641+
$indexes,
1642+
permissions: [
1643+
Permission::read(Role::any()),
1644+
Permission::create(Role::any()),
1645+
Permission::update(Role::any()),
1646+
Permission::delete(Role::any()),
1647+
],
1648+
);
1649+
1650+
$this->assertEquals($collection, $collectionDocument->getId());
1651+
$this->assertCount(3, $collectionDocument->getAttribute('attributes'));
1652+
$this->assertCount(2, $collectionDocument->getAttribute('indexes'));
1653+
1654+
$document = $database->createDocument($collection, new Document([
1655+
'$id' => 'longIdDoc',
1656+
'$permissions' => [
1657+
Permission::read(Role::any()),
1658+
Permission::update(Role::any()),
1659+
],
1660+
'name' => 'LongId Test',
1661+
'age' => 42,
1662+
'isActive' => true,
1663+
]));
1664+
1665+
$this->assertEquals('longIdDoc', $document->getId());
1666+
$this->assertEquals('LongId Test', $document->getAttribute('name'));
1667+
$this->assertEquals(42, $document->getAttribute('age'));
1668+
$this->assertTrue($document->getAttribute('isActive'));
1669+
1670+
$found = $database->find($collection, [
1671+
Query::equal('name', ['LongId Test']),
1672+
]);
1673+
1674+
$this->assertCount(1, $found);
1675+
$this->assertEquals('longIdDoc', $found[0]->getId());
1676+
1677+
$fetched = $database->getDocument($collection, 'longIdDoc');
1678+
$this->assertEquals('LongId Test', $fetched->getAttribute('name'));
1679+
1680+
$this->assertTrue($database->deleteCollection($collection));
1681+
}
15851682
}

0 commit comments

Comments
 (0)