Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 69 additions & 18 deletions src/Database/Adapter/Postgres.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
*/
class Postgres extends SQL
{
public const MAX_IDENTIFIER_NAME = 63;
/**
* @inheritDoc
*/
Expand Down Expand Up @@ -244,17 +245,24 @@ public function createCollection(string $name, array $attributes = [], array $in
";

if ($this->sharedTables) {
$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 \"{$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->getShortKey("{$namespace}_{$id}_uid");
$createdIndex = $this->getShortKey("{$namespace}_{$id}_created");
$updatedIndex = $this->getShortKey("{$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\");
";
}

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

if ($this->sharedTables) {
$uniquePermissionIndex = $this->getShortKey("{$namespace}_{$this->tenant}_{$id}_ukey");
$permissionIndex = $this->getShortKey("{$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->getShortKey("{$namespace}_{$id}_ukey");
$permissionIndex = $this->getShortKey("{$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);
";
}
Expand Down Expand Up @@ -893,15 +905,15 @@ 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->getShortKey("{$this->getNamespace()}_{$this->tenant}_{$collection}_{$id}");
$attributes = \implode(', ', $attributes);

if ($this->sharedTables && \in_array($type, [Database::INDEX_KEY, Database::INDEX_UNIQUE])) {
// Add tenant as first index column for best performance
$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) {
Expand Down Expand Up @@ -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->getShortKey("{$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()
Expand All @@ -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->getShortKey("{$namespace}_{$this->tenant}_{$collection}_{$old}");
$newIndexName = $this->getShortKey("{$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()
Expand Down Expand Up @@ -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 getShortKey(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 = md5($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->getShortKey($table);

return "{$this->quote($this->getDatabase())}.{$this->quote($table)}";
}
}
97 changes: 97 additions & 0 deletions tests/e2e/Adapter/Scopes/CollectionTests.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}