Skip to content

Commit 771548b

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix/orphaned-relationship
2 parents b1cc493 + 9e664e6 commit 771548b

2 files changed

Lines changed: 92 additions & 3 deletions

File tree

‎src/Database/Index.php‎

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,12 +132,18 @@ public static function ttl(string $key, string $attribute, int $ttl): self
132132
*/
133133
public static function fromDocument(Document $document): self
134134
{
135-
return self::hydrate(
136-
self::storedKey($document),
137-
$document->getAttribute(self::TYPE, IndexType::Key->value),
135+
[$attributes, $lengths, $orders] = self::withoutDroppedAttributes(
138136
$document->getAttribute(self::ATTRIBUTES, []),
139137
$document->getAttribute(self::LENGTHS, []),
140138
$document->getAttribute(self::ORDERS, []),
139+
);
140+
141+
return self::hydrate(
142+
self::storedKey($document),
143+
$document->getAttribute(self::TYPE, IndexType::Key->value),
144+
$attributes,
145+
$lengths,
146+
$orders,
141147
$document->getAttribute(self::TTL),
142148
);
143149
}
@@ -176,6 +182,27 @@ private static function storedKey(Document $document): mixed
176182
return \is_string($key) && $id === '_'.$key && \str_starts_with($id, '_index_') ? $id : $key;
177183
}
178184

185+
/**
186+
* 7.x left null in a stored index's attributes when an attribute it covered was deleted, while the engine dropped
187+
* that column from the index. Such an index is read as covering the attributes it still has, with the lengths and
188+
* orders at their positions.
189+
*
190+
* @return array{mixed, mixed, mixed}
191+
*/
192+
private static function withoutDroppedAttributes(mixed $attributes, mixed $lengths, mixed $orders): array
193+
{
194+
if (! \is_array($attributes) || ! \in_array(null, $attributes, true)) {
195+
return [$attributes, $lengths, $orders];
196+
}
197+
198+
$kept = \array_flip(\array_keys(\array_filter($attributes, static fn (mixed $attribute): bool => $attribute !== null)));
199+
$alongside = static fn (mixed $values): mixed => \is_array($values) && \count($values) === \count($attributes)
200+
? \array_values(\array_intersect_key($values, $kept))
201+
: $values;
202+
203+
return [\array_values(\array_intersect_key($attributes, $kept)), $alongside($lengths), $alongside($orders)];
204+
}
205+
179206
/**
180207
* @internal
181208
*/
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
namespace Tests\Unit\Indexes;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use Utopia\Database\Attribute;
7+
use Utopia\Database\Collection;
8+
use Utopia\Database\Document;
9+
use Utopia\Database\Exception\Index as IndexException;
10+
use Utopia\Database\Index;
11+
use Utopia\Query\OrderDirection;
12+
13+
final class LegacyDroppedAttributeIndexTest extends TestCase
14+
{
15+
public function testAFulltextIndexWhoseOnlyAttributeWasDeletedIsReadWithNoAttributes(): void
16+
{
17+
$index = Index::fromDocument(new Document(['$id' => 'category', 'key' => 'category', 'type' => 'fulltext', 'attributes' => [null], 'orders' => ['ASC'], 'lengths' => [200]]));
18+
19+
$this->assertSame('category', $index->key);
20+
$this->assertSame([], $index->attributes);
21+
$this->assertSame([], $index->lengths);
22+
$this->assertSame([], $index->orders);
23+
}
24+
25+
public function testAUniqueIndexKeepsTheAttributesItStillCoversWithTheirLengthsAndOrders(): void
26+
{
27+
$index = Index::fromDocument(new Document(['$id' => 'index_2', 'key' => 'index_2', 'type' => 'unique', 'attributes' => ['playId', null], 'orders' => ['ASC', 'ASC'], 'lengths' => [null, null]]));
28+
29+
$this->assertSame(['playId'], $index->attributes);
30+
$this->assertSame([null], $index->lengths);
31+
$this->assertSame([OrderDirection::Asc], $index->orders);
32+
}
33+
34+
public function testADroppedAttributeBetweenOthersKeepsEachLengthAndOrderAtItsAttribute(): void
35+
{
36+
$index = Index::fromDocument(new Document(['$id' => 'mixed', 'key' => 'mixed', 'type' => 'key', 'attributes' => ['a', null, 'c'], 'orders' => ['ASC', 'DESC', 'DESC'], 'lengths' => [10, 20, 30]]));
37+
38+
$this->assertSame(['a', 'c'], $index->attributes);
39+
$this->assertSame([10, 30], $index->lengths);
40+
$this->assertSame([OrderDirection::Asc, OrderDirection::Desc], $index->orders);
41+
}
42+
43+
public function testACollectionHoldingSuchAnIndexIsRead(): void
44+
{
45+
$collection = Collection::fromDocument(new Document([
46+
'$id' => 'plays',
47+
'name' => 'plays',
48+
'attributes' => [Attribute::string(key: 'playId', size: 36)->toDocument()],
49+
'indexes' => [new Document(['$id' => 'index_2', 'key' => 'index_2', 'type' => 'unique', 'attributes' => ['playId', null], 'orders' => ['ASC', 'ASC'], 'lengths' => [null, null]])],
50+
]));
51+
52+
$this->assertSame(['playId'], $collection->indexes()[0]->attributes);
53+
}
54+
55+
public function testAnIndexDefinedByACallerStillRefusesAnAttributeThatIsNotAString(): void
56+
{
57+
$this->expectException(IndexException::class);
58+
$this->expectExceptionMessage('Index attributes must be strings');
59+
60+
Index::fromArray(['$id' => 'bad', 'type' => 'key', 'attributes' => ['playId', null]]);
61+
}
62+
}

0 commit comments

Comments
 (0)