diff --git a/src/Database/Index.php b/src/Database/Index.php index de4012e00..2b7e3dc29 100644 --- a/src/Database/Index.php +++ b/src/Database/Index.php @@ -132,12 +132,18 @@ public static function ttl(string $key, string $attribute, int $ttl): self */ public static function fromDocument(Document $document): self { - return self::hydrate( - self::storedKey($document), - $document->getAttribute(self::TYPE, IndexType::Key->value), + [$attributes, $lengths, $orders] = self::withoutDroppedAttributes( $document->getAttribute(self::ATTRIBUTES, []), $document->getAttribute(self::LENGTHS, []), $document->getAttribute(self::ORDERS, []), + ); + + return self::hydrate( + self::storedKey($document), + $document->getAttribute(self::TYPE, IndexType::Key->value), + $attributes, + $lengths, + $orders, $document->getAttribute(self::TTL), ); } @@ -176,6 +182,27 @@ private static function storedKey(Document $document): mixed return \is_string($key) && $id === '_'.$key && \str_starts_with($id, '_index_') ? $id : $key; } + /** + * 7.x left null in a stored index's attributes when an attribute it covered was deleted, while the engine dropped + * that column from the index. Such an index is read as covering the attributes it still has, with the lengths and + * orders at their positions. + * + * @return array{mixed, mixed, mixed} + */ + private static function withoutDroppedAttributes(mixed $attributes, mixed $lengths, mixed $orders): array + { + if (! \is_array($attributes) || ! \in_array(null, $attributes, true)) { + return [$attributes, $lengths, $orders]; + } + + $kept = \array_flip(\array_keys(\array_filter($attributes, static fn (mixed $attribute): bool => $attribute !== null))); + $alongside = static fn (mixed $values): mixed => \is_array($values) && \count($values) === \count($attributes) + ? \array_values(\array_intersect_key($values, $kept)) + : $values; + + return [\array_values(\array_intersect_key($attributes, $kept)), $alongside($lengths), $alongside($orders)]; + } + /** * @internal */ diff --git a/tests/unit/Indexes/LegacyDroppedAttributeIndexTest.php b/tests/unit/Indexes/LegacyDroppedAttributeIndexTest.php new file mode 100644 index 000000000..070ef34f6 --- /dev/null +++ b/tests/unit/Indexes/LegacyDroppedAttributeIndexTest.php @@ -0,0 +1,62 @@ + 'category', 'key' => 'category', 'type' => 'fulltext', 'attributes' => [null], 'orders' => ['ASC'], 'lengths' => [200]])); + + $this->assertSame('category', $index->key); + $this->assertSame([], $index->attributes); + $this->assertSame([], $index->lengths); + $this->assertSame([], $index->orders); + } + + public function testAUniqueIndexKeepsTheAttributesItStillCoversWithTheirLengthsAndOrders(): void + { + $index = Index::fromDocument(new Document(['$id' => 'index_2', 'key' => 'index_2', 'type' => 'unique', 'attributes' => ['playId', null], 'orders' => ['ASC', 'ASC'], 'lengths' => [null, null]])); + + $this->assertSame(['playId'], $index->attributes); + $this->assertSame([null], $index->lengths); + $this->assertSame([OrderDirection::Asc], $index->orders); + } + + public function testADroppedAttributeBetweenOthersKeepsEachLengthAndOrderAtItsAttribute(): void + { + $index = Index::fromDocument(new Document(['$id' => 'mixed', 'key' => 'mixed', 'type' => 'key', 'attributes' => ['a', null, 'c'], 'orders' => ['ASC', 'DESC', 'DESC'], 'lengths' => [10, 20, 30]])); + + $this->assertSame(['a', 'c'], $index->attributes); + $this->assertSame([10, 30], $index->lengths); + $this->assertSame([OrderDirection::Asc, OrderDirection::Desc], $index->orders); + } + + public function testACollectionHoldingSuchAnIndexIsRead(): void + { + $collection = Collection::fromDocument(new Document([ + '$id' => 'plays', + 'name' => 'plays', + 'attributes' => [Attribute::string(key: 'playId', size: 36)->toDocument()], + 'indexes' => [new Document(['$id' => 'index_2', 'key' => 'index_2', 'type' => 'unique', 'attributes' => ['playId', null], 'orders' => ['ASC', 'ASC'], 'lengths' => [null, null]])], + ])); + + $this->assertSame(['playId'], $collection->indexes()[0]->attributes); + } + + public function testAnIndexDefinedByACallerStillRefusesAnAttributeThatIsNotAString(): void + { + $this->expectException(IndexException::class); + $this->expectExceptionMessage('Index attributes must be strings'); + + Index::fromArray(['$id' => 'bad', 'type' => 'key', 'attributes' => ['playId', null]]); + } +}