Skip to content

Commit f9b6c58

Browse files
authored
Merge pull request #637 from utopia-php/fix-many-select
Fix many to many select across collections
2 parents a29d70d + 175757f commit f9b6c58

File tree

3 files changed

+229
-12
lines changed

3 files changed

+229
-12
lines changed

docker-compose.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ services:
1717
- ./dev/xdebug.ini:/usr/local/etc/php/conf.d/xdebug.ini
1818
- /var/run/docker.sock:/var/run/docker.sock
1919
- ./docker-compose.yml:/usr/src/code/docker-compose.yml
20+
environment:
21+
PHP_IDE_CONFIG: serverName=tests
2022

2123
adminer:
2224
image: adminer

src/Database/Database.php

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3520,13 +3520,30 @@ private function populateDocumentRelationships(Document $collection, Document $d
35203520
Query::limit(PHP_INT_MAX)
35213521
]));
35223522

3523-
$related = [];
3523+
$relatedIds = [];
35243524
foreach ($junctions as $junction) {
3525-
$related[] = $this->getDocument(
3526-
$relatedCollection->getId(),
3527-
$junction->getAttribute($key),
3528-
$queries
3529-
);
3525+
$relatedIds[] = $junction->getAttribute($key);
3526+
}
3527+
3528+
$related = [];
3529+
if (!empty($relatedIds)) {
3530+
$foundRelated = $this->find($relatedCollection->getId(), [
3531+
Query::equal('$id', $relatedIds),
3532+
Query::limit(PHP_INT_MAX),
3533+
...$queries
3534+
]);
3535+
3536+
// Preserve the order of related documents to match the junction order
3537+
$relatedById = [];
3538+
foreach ($foundRelated as $doc) {
3539+
$relatedById[$doc->getId()] = $doc;
3540+
}
3541+
3542+
foreach ($relatedIds as $relatedId) {
3543+
if (isset($relatedById[$relatedId])) {
3544+
$related[] = $relatedById[$relatedId];
3545+
}
3546+
}
35303547
}
35313548

35323549
$this->relationshipFetchDepth--;
@@ -4098,7 +4115,6 @@ public function updateDocument(string $collection, string $id, Document $documen
40984115
if ($this->adapter->getSharedTables()) {
40994116
$document['$tenant'] = $old->getTenant(); // Make sure user doesn't switch tenant
41004117
}
4101-
41024118
$document = new Document($document);
41034119

41044120
$relationships = \array_filter($collection->getAttribute('attributes', []), function ($attribute) {
@@ -4335,7 +4351,7 @@ public function updateDocuments(
43354351
$cursor = $grouped['cursor'];
43364352

43374353
if (!empty($cursor) && $cursor->getCollection() !== $collection->getId()) {
4338-
throw new DatabaseException("cursor Document must be from the same Collection.");
4354+
throw new DatabaseException("Cursor document must be from the same Collection.");
43394355
}
43404356

43414357
unset($updates['$id']);
@@ -4485,8 +4501,8 @@ private function updateDocumentRelationships(Document $collection, Document $old
44854501

44864502
if ($oldValue == $value) {
44874503
if (
4488-
($relationType === Database::RELATION_ONE_TO_ONE ||
4489-
($relationType === Database::RELATION_MANY_TO_ONE && $side === Database::RELATION_SIDE_PARENT)) &&
4504+
($relationType === Database::RELATION_ONE_TO_ONE
4505+
|| ($relationType === Database::RELATION_MANY_TO_ONE && $side === Database::RELATION_SIDE_PARENT)) &&
44904506
$value instanceof Document
44914507
) {
44924508
$document->setAttribute($key, $value->getId());
@@ -4612,7 +4628,7 @@ private function updateDocumentRelationships(Document $collection, Document $old
46124628
$this->skipRelationships(fn () => $this->updateDocument(
46134629
$relatedCollection->getId(),
46144630
$oldRelated->getId(),
4615-
$oldRelated->setAttribute($twoWayKey, null)
4631+
new Document([$twoWayKey => null])
46164632
));
46174633
}
46184634
break;
@@ -4638,7 +4654,7 @@ private function updateDocumentRelationships(Document $collection, Document $old
46384654
} elseif ($item instanceof Document) {
46394655
return $item->getId();
46404656
} else {
4641-
throw new RelationshipException('Invalid relationship value. No ID provided.');
4657+
throw new RelationshipException('Invalid relationship value. Must be either a document or document ID.');
46424658
}
46434659
}, $value);
46444660

tests/e2e/Adapter/Scopes/Relationships/ManyToManyTests.php

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1519,6 +1519,205 @@ public function testRecreateManyToManyOneWayRelationshipFromParent(): void
15191519
$database->deleteCollection('two');
15201520
}
15211521

1522+
public function testSelectManyToMany(): void
1523+
{
1524+
/** @var Database $database */
1525+
$database = static::getDatabase();
1526+
1527+
if (!$database->getAdapter()->getSupportForRelationships()) {
1528+
$this->expectNotToPerformAssertions();
1529+
return;
1530+
}
1531+
1532+
$database->createCollection('select_m2m_collection1');
1533+
$database->createCollection('select_m2m_collection2');
1534+
1535+
$database->createAttribute('select_m2m_collection1', 'name', Database::VAR_STRING, 255, true);
1536+
$database->createAttribute('select_m2m_collection1', 'type', Database::VAR_STRING, 255, true);
1537+
$database->createAttribute('select_m2m_collection2', 'name', Database::VAR_STRING, 255, true);
1538+
$database->createAttribute('select_m2m_collection2', 'type', Database::VAR_STRING, 255, true);
1539+
1540+
// Many-to-Many Relationship
1541+
$database->createRelationship(
1542+
collection: 'select_m2m_collection1',
1543+
relatedCollection: 'select_m2m_collection2',
1544+
type: Database::RELATION_MANY_TO_MANY,
1545+
twoWay: true
1546+
);
1547+
1548+
// Create documents in the first collection
1549+
$doc1 = $database->createDocument('select_m2m_collection1', new Document([
1550+
'$id' => 'doc1',
1551+
'$permissions' => [
1552+
Permission::read(Role::any()),
1553+
Permission::update(Role::any()),
1554+
Permission::delete(Role::any()),
1555+
],
1556+
'name' => 'Document 1',
1557+
'type' => 'Type A',
1558+
'select_m2m_collection2' => [
1559+
[
1560+
'$id' => 'related_doc1',
1561+
'$permissions' => [
1562+
Permission::read(Role::any()),
1563+
Permission::update(Role::any()),
1564+
Permission::delete(Role::any()),
1565+
],
1566+
'name' => 'Related Document 1',
1567+
'type' => 'Type B',
1568+
],
1569+
[
1570+
'$id' => 'related_doc2',
1571+
'$permissions' => [
1572+
Permission::read(Role::any()),
1573+
Permission::update(Role::any()),
1574+
Permission::delete(Role::any()),
1575+
],
1576+
'name' => 'Related Document 2',
1577+
'type' => 'Type C',
1578+
],
1579+
],
1580+
]));
1581+
1582+
// Use select query to get only name of the related documents
1583+
$docs = $database->find('select_m2m_collection1', [
1584+
Query::select(['name', 'select_m2m_collection2.name']),
1585+
]);
1586+
1587+
$this->assertCount(1, $docs);
1588+
$this->assertEquals('Document 1', $docs[0]->getAttribute('name'));
1589+
$this->assertArrayNotHasKey('type', $docs[0]);
1590+
1591+
$relatedDocs = $docs[0]->getAttribute('select_m2m_collection2');
1592+
1593+
$this->assertCount(2, $relatedDocs);
1594+
$this->assertEquals('Related Document 1', $relatedDocs[0]->getAttribute('name'));
1595+
$this->assertEquals('Related Document 2', $relatedDocs[1]->getAttribute('name'));
1596+
$this->assertArrayNotHasKey('type', $relatedDocs[0]);
1597+
$this->assertArrayNotHasKey('type', $relatedDocs[1]);
1598+
}
1599+
1600+
public function testSelectAcrossMultipleCollections(): void
1601+
{
1602+
/** @var Database $database */
1603+
$database = static::getDatabase();
1604+
1605+
if (!$database->getAdapter()->getSupportForRelationships()) {
1606+
$this->expectNotToPerformAssertions();
1607+
return;
1608+
}
1609+
1610+
// Create collections
1611+
$database->createCollection('artists', permissions: [
1612+
Permission::read(Role::any()),
1613+
Permission::create(Role::any()),
1614+
Permission::update(Role::any()),
1615+
Permission::delete(Role::any())
1616+
], documentSecurity: false);
1617+
$database->createCollection('albums', permissions: [
1618+
Permission::read(Role::any()),
1619+
Permission::create(Role::any()),
1620+
Permission::update(Role::any()),
1621+
Permission::delete(Role::any())
1622+
], documentSecurity: false);
1623+
$database->createCollection('tracks', permissions: [
1624+
Permission::read(Role::any()),
1625+
Permission::create(Role::any()),
1626+
Permission::update(Role::any()),
1627+
Permission::delete(Role::any())
1628+
], documentSecurity: false);
1629+
1630+
// Add attributes
1631+
$database->createAttribute('artists', 'name', Database::VAR_STRING, 255, true);
1632+
$database->createAttribute('albums', 'name', Database::VAR_STRING, 255, true);
1633+
$database->createAttribute('tracks', 'title', Database::VAR_STRING, 255, true);
1634+
$database->createAttribute('tracks', 'duration', Database::VAR_INTEGER, 0, true);
1635+
1636+
// Create relationships
1637+
$database->createRelationship(
1638+
collection: 'artists',
1639+
relatedCollection: 'albums',
1640+
type: Database::RELATION_MANY_TO_MANY,
1641+
twoWay: true
1642+
);
1643+
1644+
$database->createRelationship(
1645+
collection: 'albums',
1646+
relatedCollection: 'tracks',
1647+
type: Database::RELATION_MANY_TO_MANY,
1648+
twoWay: true
1649+
);
1650+
1651+
// Create documents
1652+
$database->createDocument('artists', new Document([
1653+
'$id' => 'artist1',
1654+
'name' => 'The Great Artist',
1655+
'albums' => [
1656+
[
1657+
'$id' => 'album1',
1658+
'name' => 'First Album',
1659+
'tracks' => [
1660+
[
1661+
'$id' => 'track1',
1662+
'title' => 'Hit Song 1',
1663+
'duration' => 180,
1664+
],
1665+
[
1666+
'$id' => 'track2',
1667+
'title' => 'Hit Song 2',
1668+
'duration' => 220,
1669+
]
1670+
]
1671+
],
1672+
[
1673+
'$id' => 'album2',
1674+
'name' => 'Second Album',
1675+
'tracks' => [
1676+
[
1677+
'$id' => 'track3',
1678+
'title' => 'Ballad 3',
1679+
'duration' => 240,
1680+
]
1681+
]
1682+
]
1683+
]
1684+
]));
1685+
1686+
// Query with nested select
1687+
$artists = $database->find('artists', [
1688+
Query::select(['name', 'albums.name', 'albums.tracks.title'])
1689+
]);
1690+
1691+
$this->assertCount(1, $artists);
1692+
$artist = $artists[0];
1693+
$this->assertEquals('The Great Artist', $artist->getAttribute('name'));
1694+
$this->assertArrayHasKey('albums', $artist->getArrayCopy());
1695+
1696+
$albums = $artist->getAttribute('albums');
1697+
$this->assertCount(2, $albums);
1698+
1699+
$album1 = $albums[0];
1700+
$this->assertEquals('First Album', $album1->getAttribute('name'));
1701+
$this->assertArrayHasKey('tracks', $album1->getArrayCopy());
1702+
$this->assertArrayNotHasKey('artists', $album1->getArrayCopy());
1703+
1704+
$album2 = $albums[1];
1705+
$this->assertEquals('Second Album', $album2->getAttribute('name'));
1706+
$this->assertArrayHasKey('tracks', $album2->getArrayCopy());
1707+
1708+
$album1Tracks = $album1->getAttribute('tracks');
1709+
$this->assertCount(2, $album1Tracks);
1710+
$this->assertEquals('Hit Song 1', $album1Tracks[0]->getAttribute('title'));
1711+
$this->assertArrayNotHasKey('duration', $album1Tracks[0]->getArrayCopy());
1712+
$this->assertEquals('Hit Song 2', $album1Tracks[1]->getAttribute('title'));
1713+
$this->assertArrayNotHasKey('duration', $album1Tracks[1]->getArrayCopy());
1714+
1715+
$album2Tracks = $album2->getAttribute('tracks');
1716+
$this->assertCount(1, $album2Tracks);
1717+
$this->assertEquals('Ballad 3', $album2Tracks[0]->getAttribute('title'));
1718+
$this->assertArrayNotHasKey('duration', $album2Tracks[0]->getArrayCopy());
1719+
}
1720+
15221721
public function testDeleteBulkDocumentsManyToManyRelationship(): void
15231722
{
15241723
/** @var Database $database */

0 commit comments

Comments
 (0)