Skip to content

Commit 038cd56

Browse files
abnegateclaude
andcommitted
fix: add getSchemaIndexes and validate orphan indexes in createIndex
Adds getSchemaIndexes() to match the existing getSchemaAttributes() pattern. When an index exists in physical schema but not metadata (orphan), createIndex now validates the physical definition matches. Mismatched orphans are dropped and recreated. - Adapter: abstract getSchemaIndexes() + getSupportForSchemaIndexes() - MariaDB: queries INFORMATION_SCHEMA.STATISTICS - All others: default stubs (empty array / false) - Database::createIndex: validates orphan index shape before reuse Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 20ba253 commit 038cd56

8 files changed

Lines changed: 154 additions & 9 deletions

File tree

src/Database/Adapter.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -958,6 +958,13 @@ abstract public function getSupportForAttributes(): bool;
958958
*/
959959
abstract public function getSupportForSchemaAttributes(): bool;
960960

961+
/**
962+
* Are schema indexes supported?
963+
*
964+
* @return bool
965+
*/
966+
abstract public function getSupportForSchemaIndexes(): bool;
967+
961968
/**
962969
* Is index supported?
963970
*
@@ -1365,6 +1372,17 @@ abstract public function getInternalIndexesKeys(): array;
13651372
*/
13661373
abstract public function getSchemaAttributes(string $collection): array;
13671374

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

src/Database/Adapter/MariaDB.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1845,6 +1845,58 @@ public function getSupportForSchemaAttributes(): bool
18451845
return true;
18461846
}
18471847

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

src/Database/Adapter/Mongo.php

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

3604+
public function getSupportForSchemaIndexes(): bool
3605+
{
3606+
return false;
3607+
}
3608+
3609+
public function getSchemaIndexes(string $collection): array
3610+
{
3611+
return [];
3612+
}
3613+
36043614
/**
36053615
* @param string $collection
36063616
* @param array<int|string> $tenants

src/Database/Adapter/Pool.php

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

566+
public function getSupportForSchemaIndexes(): bool
567+
{
568+
return $this->delegate(__FUNCTION__, \func_get_args());
569+
}
570+
571+
public function getSchemaIndexes(string $collection): array
572+
{
573+
return $this->delegate(__FUNCTION__, \func_get_args());
574+
}
575+
566576
public function getTenantQuery(string $collection, string $alias = ''): string
567577
{
568578
return $this->delegate(__FUNCTION__, \func_get_args());

src/Database/Adapter/Postgres.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2146,6 +2146,11 @@ public function getSupportForSchemaAttributes(): bool
21462146
return false;
21472147
}
21482148

2149+
public function getSupportForSchemaIndexes(): bool
2150+
{
2151+
return false;
2152+
}
2153+
21492154
public function getSupportForUpserts(): bool
21502155
{
21512156
return true;

src/Database/Adapter/SQL.php

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

2331+
public function getSchemaIndexes(string $collection): array
2332+
{
2333+
return [];
2334+
}
2335+
2336+
public function getSupportForSchemaIndexes(): bool
2337+
{
2338+
return false;
2339+
}
2340+
23312341
public function getTenantQuery(
23322342
string $collection,
23332343
string $alias = '',

src/Database/Adapter/SQLite.php

Lines changed: 5 additions & 0 deletions
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
*

src/Database/Database.php

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4574,18 +4574,48 @@ public function createIndex(string $collection, string $id, string $type, array
45744574
}
45754575

45764576
$created = false;
4577+
$existsInSchema = false;
45774578

4578-
try {
4579-
$created = $this->adapter->createIndex($collection->getId(), $id, $type, $attributes, $lengths, $orders, $indexAttributesWithTypes, [], $ttl);
4579+
if ($this->adapter->getSupportForSchemaIndexes()) {
4580+
$schemaIndexes = $this->getSchemaIndexes($collection->getId());
4581+
$filteredId = $this->adapter->filter($id);
45804582

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

45914621
$collection->setAttribute('indexes', $index, Document::SET_TYPE_APPEND);
@@ -9220,6 +9250,11 @@ public function getSchemaAttributes(string $collection): array
92209250
return $this->adapter->getSchemaAttributes($collection);
92219251
}
92229252

9253+
public function getSchemaIndexes(string $collection): array
9254+
{
9255+
return $this->adapter->getSchemaIndexes($collection);
9256+
}
9257+
92239258
/**
92249259
* @param string $collectionId
92259260
* @param string|null $documentId

0 commit comments

Comments
 (0)