Skip to content

Commit 78c490a

Browse files
committed
Merge branch 'main' of github.com:utopia-php/database into joins9
2 parents 0fdc9af + f121418 commit 78c490a

9 files changed

Lines changed: 356 additions & 26 deletions

File tree

src/Database/Adapter.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -979,6 +979,13 @@ abstract public function getSupportForAttributes(): bool;
979979
*/
980980
abstract public function getSupportForSchemaAttributes(): bool;
981981

982+
/**
983+
* Are schema indexes supported?
984+
*
985+
* @return bool
986+
*/
987+
abstract public function getSupportForSchemaIndexes(): bool;
988+
982989
/**
983990
* Is index supported?
984991
*
@@ -1367,6 +1374,17 @@ abstract public function getInternalIndexesKeys(): array;
13671374
*/
13681375
abstract public function getSchemaAttributes(string $collection): array;
13691376

1377+
/**
1378+
* Get Schema Indexes
1379+
*
1380+
* Returns physical index definitions from the database schema.
1381+
*
1382+
* @param string $collection
1383+
* @return array<Document>
1384+
* @throws DatabaseException
1385+
*/
1386+
abstract public function getSchemaIndexes(string $collection): array;
1387+
13701388
/**
13711389
* Get the expected column type for a given attribute type.
13721390
*

src/Database/Adapter/MariaDB.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1854,6 +1854,58 @@ public function getSupportForSchemaAttributes(): bool
18541854
return true;
18551855
}
18561856

1857+
public function getSupportForSchemaIndexes(): bool
1858+
{
1859+
return true;
1860+
}
1861+
1862+
public function getSchemaIndexes(string $collection): array
1863+
{
1864+
$schema = $this->getDatabase();
1865+
$collection = $this->getNamespace() . '_' . $this->filter($collection);
1866+
1867+
try {
1868+
$stmt = $this->getPDO()->prepare('
1869+
SELECT
1870+
INDEX_NAME as indexName,
1871+
COLUMN_NAME as columnName,
1872+
NON_UNIQUE as nonUnique,
1873+
SEQ_IN_INDEX as seqInIndex,
1874+
INDEX_TYPE as indexType,
1875+
SUB_PART as subPart
1876+
FROM INFORMATION_SCHEMA.STATISTICS
1877+
WHERE TABLE_SCHEMA = :schema AND TABLE_NAME = :table
1878+
ORDER BY INDEX_NAME, SEQ_IN_INDEX
1879+
');
1880+
$stmt->bindParam(':schema', $schema);
1881+
$stmt->bindParam(':table', $collection);
1882+
$stmt->execute();
1883+
$rows = $stmt->fetchAll();
1884+
$stmt->closeCursor();
1885+
1886+
$grouped = [];
1887+
foreach ($rows as $row) {
1888+
$name = $row['indexName'];
1889+
if (!isset($grouped[$name])) {
1890+
$grouped[$name] = [
1891+
'$id' => $name,
1892+
'indexName' => $name,
1893+
'indexType' => $row['indexType'],
1894+
'nonUnique' => (int)$row['nonUnique'],
1895+
'columns' => [],
1896+
'lengths' => [],
1897+
];
1898+
}
1899+
$grouped[$name]['columns'][] = $row['columnName'];
1900+
$grouped[$name]['lengths'][] = $row['subPart'] !== null ? (int)$row['subPart'] : null;
1901+
}
1902+
1903+
return \array_map(fn ($idx) => new Document($idx), \array_values($grouped));
1904+
} catch (PDOException $e) {
1905+
throw new DatabaseException('Failed to get schema indexes', $e->getCode(), $e);
1906+
}
1907+
}
1908+
18571909
/**
18581910
* Set max execution time
18591911
* @param int $milliseconds

src/Database/Adapter/Mongo.php

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -414,8 +414,10 @@ public function createCollection(string $name, array $attributes = [], array $in
414414
{
415415
$id = $this->getNamespace() . '_' . $this->filter($name);
416416

417-
// For metadata collections outside transactions, check if exists first
418-
if (!$this->inTransaction && $name === Database::METADATA && $this->exists($this->getNamespace(), $name)) {
417+
// In shared-tables mode or for metadata, the physical collection may
418+
// already exist for another tenant. Return early to avoid a
419+
// "Collection Exists" exception from the client.
420+
if (!$this->inTransaction && ($this->getSharedTables() || $name === Database::METADATA) && $this->exists($this->getNamespace(), $name)) {
419421
return true;
420422
}
421423

@@ -428,6 +430,16 @@ public function createCollection(string $name, array $attributes = [], array $in
428430
if ($e instanceof DuplicateException) {
429431
return true;
430432
}
433+
// Client throws code-0 "Collection Exists" when its pre-check
434+
// finds the collection. In shared-tables/metadata context this
435+
// is a no-op; otherwise re-throw as DuplicateException so
436+
// Database::createCollection() can run orphan reconciliation.
437+
if ($e->getCode() === 0 && stripos($e->getMessage(), 'Collection Exists') !== false) {
438+
if ($this->getSharedTables() || $name === Database::METADATA) {
439+
return true;
440+
}
441+
throw new DuplicateException('Collection already exists', $e->getCode(), $e);
442+
}
431443
throw $e;
432444
}
433445

@@ -3667,6 +3679,16 @@ public function getSchemaAttributes(string $collection): array
36673679
return [];
36683680
}
36693681

3682+
public function getSupportForSchemaIndexes(): bool
3683+
{
3684+
return false;
3685+
}
3686+
3687+
public function getSchemaIndexes(string $collection): array
3688+
{
3689+
return [];
3690+
}
3691+
36703692
/**
36713693
* @param string $collection
36723694
* @param array<int|string> $tenants

src/Database/Adapter/Pool.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,16 @@ public function getSchemaAttributes(string $collection): array
584584
return $this->delegate(__FUNCTION__, \func_get_args());
585585
}
586586

587+
public function getSupportForSchemaIndexes(): bool
588+
{
589+
return $this->delegate(__FUNCTION__, \func_get_args());
590+
}
591+
592+
public function getSchemaIndexes(string $collection): array
593+
{
594+
return $this->delegate(__FUNCTION__, \func_get_args());
595+
}
596+
587597
public function getTenantQuery(string $collection, string $alias = ''): string
588598
{
589599
return $this->delegate(__FUNCTION__, \func_get_args());

src/Database/Adapter/Postgres.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,11 @@ public function deleteCollection(string $id): bool
439439
$sql = "DROP TABLE {$this->getSQLTable($id)}, {$this->getSQLTable($id . '_perms')}";
440440
$sql = $this->trigger(Database::EVENT_COLLECTION_DELETE, $sql);
441441

442-
return $this->getPDO()->prepare($sql)->execute();
442+
try {
443+
return $this->getPDO()->prepare($sql)->execute();
444+
} catch (PDOException $e) {
445+
throw $this->processException($e);
446+
}
443447
}
444448

445449
/**
@@ -2152,6 +2156,11 @@ public function getSupportForSchemaAttributes(): bool
21522156
return false;
21532157
}
21542158

2159+
public function getSupportForSchemaIndexes(): bool
2160+
{
2161+
return false;
2162+
}
2163+
21552164
public function getSupportForUpserts(): bool
21562165
{
21572166
return true;
@@ -2239,6 +2248,11 @@ protected function processException(PDOException $e): \Exception
22392248
return new LimitException('Datetime field overflow', $e->getCode(), $e);
22402249
}
22412250

2251+
// Unknown table
2252+
if ($e->getCode() === '42P01' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 7) {
2253+
return new NotFoundException('Collection not found', $e->getCode(), $e);
2254+
}
2255+
22422256
// Unknown column
22432257
if ($e->getCode() === "42703" && isset($e->errorInfo[1]) && $e->errorInfo[1] === 7) {
22442258
return new NotFoundException('Attribute not found', $e->getCode(), $e);

src/Database/Adapter/SQL.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2339,6 +2339,16 @@ public function getSchemaAttributes(string $collection): array
23392339
return [];
23402340
}
23412341

2342+
public function getSchemaIndexes(string $collection): array
2343+
{
2344+
return [];
2345+
}
2346+
2347+
public function getSupportForSchemaIndexes(): bool
2348+
{
2349+
return false;
2350+
}
2351+
23422352
public function getTenantQuery(
23432353
string $collection,
23442354
string $alias = '',

src/Database/Adapter/SQLite.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -974,6 +974,11 @@ public function getSupportForSchemaAttributes(): bool
974974
return false;
975975
}
976976

977+
public function getSupportForSchemaIndexes(): bool
978+
{
979+
return false;
980+
}
981+
977982
/**
978983
* Is upsert supported?
979984
*
@@ -1312,6 +1317,16 @@ protected function processException(PDOException $e): \Exception
13121317
return new TimeoutException('Query timed out', $e->getCode(), $e);
13131318
}
13141319

1320+
// Table/index already exists (SQLITE_ERROR with "already exists" message)
1321+
if ($e->getCode() === 'HY000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1 && stripos($e->getMessage(), 'already exists') !== false) {
1322+
return new DuplicateException('Collection already exists', $e->getCode(), $e);
1323+
}
1324+
1325+
// Table not found (SQLITE_ERROR with "no such table" message)
1326+
if ($e->getCode() === 'HY000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1 && stripos($e->getMessage(), 'no such table') !== false) {
1327+
return new NotFoundException('Collection not found', $e->getCode(), $e);
1328+
}
1329+
13151330
// Duplicate - SQLite uses various error codes for constraint violations:
13161331
// - Error code 19 is SQLITE_CONSTRAINT (includes UNIQUE violations)
13171332
// - Error code 1 is also used for some duplicate cases
@@ -1320,7 +1335,6 @@ protected function processException(PDOException $e): \Exception
13201335
($e->getCode() === 'HY000' && isset($e->errorInfo[1]) && ($e->errorInfo[1] === 1 || $e->errorInfo[1] === 19)) ||
13211336
$e->getCode() === '23000'
13221337
) {
1323-
// Check if it's actually a duplicate/unique constraint violation
13241338
$message = $e->getMessage();
13251339
if (
13261340
(isset($e->errorInfo[1]) && $e->errorInfo[1] === 19) ||

src/Database/Database.php

Lines changed: 76 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1794,19 +1794,31 @@ public function createCollection(string $id, array $attributes = [], array $inde
17941794
}
17951795
}
17961796

1797+
$createdPhysicalTable = false;
1798+
17971799
try {
17981800
$this->adapter->createCollection($id, $attributes, $indexes);
1801+
$createdPhysicalTable = true;
17991802
} catch (DuplicateException $e) {
1800-
// Metadata check (above) already verified collection is absent
1801-
// from metadata. A DuplicateException from the adapter means the
1802-
// collection exists only in physical schema — an orphan from a prior
1803-
// partial failure. Drop and recreate to ensure schema matches.
1804-
try {
1805-
$this->adapter->deleteCollection($id);
1806-
} catch (NotFoundException) {
1807-
// Already removed by a concurrent reconciler.
1803+
if ($this->adapter->getSharedTables()
1804+
&& ($id === self::METADATA || $this->adapter->exists($this->adapter->getDatabase(), $id))) {
1805+
// In shared-tables mode the physical table is reused across
1806+
// tenants. A DuplicateException simply means the table already
1807+
// exists for another tenant — not an orphan.
1808+
} else {
1809+
// Metadata check (above) already verified collection is absent
1810+
// from metadata. A DuplicateException from the adapter means
1811+
// the collection exists only in physical schema — an orphan
1812+
// from a prior partial failure. Drop and recreate to ensure
1813+
// schema matches.
1814+
try {
1815+
$this->adapter->deleteCollection($id);
1816+
} catch (NotFoundException) {
1817+
// Already removed by a concurrent reconciler.
1818+
}
1819+
$this->adapter->createCollection($id, $attributes, $indexes);
1820+
$createdPhysicalTable = true;
18081821
}
1809-
$this->adapter->createCollection($id, $attributes, $indexes);
18101822
}
18111823

18121824
if ($id === self::METADATA) {
@@ -1816,10 +1828,12 @@ public function createCollection(string $id, array $attributes = [], array $inde
18161828
try {
18171829
$createdCollection = $this->silent(fn () => $this->createDocument(self::METADATA, $collection));
18181830
} catch (\Throwable $e) {
1819-
try {
1820-
$this->cleanupCollection($id);
1821-
} catch (\Throwable $e) {
1822-
Console::error("Failed to rollback collection '{$id}': " . $e->getMessage());
1831+
if ($createdPhysicalTable) {
1832+
try {
1833+
$this->cleanupCollection($id);
1834+
} catch (\Throwable $e) {
1835+
Console::error("Failed to rollback collection '{$id}': " . $e->getMessage());
1836+
}
18231837
}
18241838
throw new DatabaseException("Failed to create collection metadata for '{$id}': " . $e->getMessage(), previous: $e);
18251839
}
@@ -4564,18 +4578,49 @@ public function createIndex(string $collection, string $id, string $type, array
45644578
}
45654579

45664580
$created = false;
4581+
$existsInSchema = false;
45674582

4568-
try {
4569-
$created = $this->adapter->createIndex($collection->getId(), $id, $type, $attributes, $lengths, $orders, $indexAttributesWithTypes, [], $ttl);
4583+
if ($this->adapter->getSupportForSchemaIndexes()
4584+
&& !($this->adapter->getSharedTables() && $this->isMigrating())) {
4585+
$schemaIndexes = $this->getSchemaIndexes($collection->getId());
4586+
$filteredId = $this->adapter->filter($id);
45704587

4571-
if (!$created) {
4572-
throw new DatabaseException('Failed to create index');
4588+
foreach ($schemaIndexes as $schemaIndex) {
4589+
if (\strtolower($schemaIndex->getId()) === \strtolower($filteredId)) {
4590+
$schemaColumns = $schemaIndex->getAttribute('columns', []);
4591+
$schemaLengths = $schemaIndex->getAttribute('lengths', []);
4592+
4593+
$filteredAttributes = \array_map(fn ($a) => $this->adapter->filter($a), $attributes);
4594+
$match = ($schemaColumns === $filteredAttributes && $schemaLengths === $lengths);
4595+
4596+
if ($match) {
4597+
$existsInSchema = true;
4598+
} else {
4599+
// Orphan index with wrong definition — drop so it
4600+
// gets recreated with the correct shape.
4601+
try {
4602+
$this->adapter->deleteIndex($collection->getId(), $id);
4603+
} catch (NotFoundException) {
4604+
}
4605+
}
4606+
break;
4607+
}
4608+
}
4609+
}
4610+
4611+
if (!$existsInSchema) {
4612+
try {
4613+
$created = $this->adapter->createIndex($collection->getId(), $id, $type, $attributes, $lengths, $orders, $indexAttributesWithTypes, [], $ttl);
4614+
4615+
if (!$created) {
4616+
throw new DatabaseException('Failed to create index');
4617+
}
4618+
} catch (DuplicateException) {
4619+
// Metadata check (lines above) already verified index is absent
4620+
// from metadata. A DuplicateException from the adapter means the
4621+
// index exists only in physical schema — an orphan from a prior
4622+
// partial failure. Skip creation and proceed to metadata update.
45734623
}
4574-
} catch (DuplicateException $e) {
4575-
// Metadata check (lines above) already verified index is absent
4576-
// from metadata. A DuplicateException from the adapter means the
4577-
// index exists only in physical schema — an orphan from a prior
4578-
// partial failure. Skip creation and proceed to metadata update.
45794624
}
45804625

45814626
$collection->setAttribute('indexes', $index, Document::SET_TYPE_APPEND);
@@ -9265,6 +9310,15 @@ public function getSchemaAttributes(string $collection): array
92659310
return $this->adapter->getSchemaAttributes($collection);
92669311
}
92679312

9313+
/**
9314+
* @param string $collection
9315+
* @return array<Document>
9316+
*/
9317+
public function getSchemaIndexes(string $collection): array
9318+
{
9319+
return $this->adapter->getSchemaIndexes($collection);
9320+
}
9321+
92689322
/**
92699323
* @param string $collectionId
92709324
* @param string|null $documentId

0 commit comments

Comments
 (0)