Skip to content

Commit 7cfd773

Browse files
authored
Better heap sync (#180)
* Fix test * Better sync * hotfix * Remove SplObjectStorage cloning * Add heap test
1 parent 66d04a0 commit 7cfd773

File tree

6 files changed

+258
-29
lines changed

6 files changed

+258
-29
lines changed

src/Heap/Heap.php

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,6 @@ public function __destruct()
3939
$this->clean();
4040
}
4141

42-
/**
43-
* @return SplObjectStorage
44-
*/
4542
public function getIterator(): SplObjectStorage
4643
{
4744
return $this->storage;
@@ -95,6 +92,11 @@ public function find(string $role, array $scope)
9592
public function attach($entity, Node $node, array $index = []): void
9693
{
9794
$this->storage->offsetSet($entity, $node);
95+
$role = $node->getRole();
96+
97+
if ($node->hasState()) {
98+
$this->eraseIndexes($role, $node->getInitialData(), $entity);
99+
}
98100

99101
$data = $node->getData();
100102
foreach ($index as $key) {
@@ -113,8 +115,7 @@ public function attach($entity, Node $node, array $index = []): void
113115
$value = (string)$data[$key];
114116
}
115117

116-
$this->paths[get_class($entity)][$key][$value] = $entity;
117-
$this->paths[$node->getRole()][$key][$value] = $entity;
118+
$this->paths[$role][$key][$value] = $entity;
118119
}
119120
}
120121

@@ -130,12 +131,9 @@ public function detach($entity): void
130131

131132
$role = $node->getRole();
132133

133-
// erase all the indexes
134-
if (isset($this->paths[$role])) {
135-
$keys = array_keys($this->paths[$role]);
136-
foreach ($keys as $key) {
137-
unset($this->paths[$role][$key][$node->getData()[$key]]);
138-
}
134+
$this->eraseIndexes($role, $node->getData(), $entity);
135+
if ($node->hasState()) {
136+
$this->eraseIndexes($role, $node->getInitialData(), $entity);
139137
}
140138

141139
$this->storage->offsetUnset($entity);
@@ -149,4 +147,22 @@ public function clean(): void
149147
$this->paths = [];
150148
$this->storage = new \SplObjectStorage();
151149
}
150+
151+
private function eraseIndexes(string $role, array $data, object $entity): void
152+
{
153+
if (!isset($this->paths[$role]) || empty($data)) {
154+
return;
155+
}
156+
$keys = array_keys($this->paths[$role]);
157+
foreach ($keys as $key) {
158+
$value = isset($data[$key]) ? (string)$data[$key] : null;
159+
if ($value === null) {
160+
continue;
161+
}
162+
$current = &$this->paths[$role][$key];
163+
if (isset($current[$value]) && $current[$value] === $entity) {
164+
unset($current[$value]);
165+
}
166+
}
167+
}
152168
}

src/Heap/Node.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,11 @@ public function getState(): State
8888
return $this->state;
8989
}
9090

91+
public function hasState(): bool
92+
{
93+
return $this->state !== null;
94+
}
95+
9196
/**
9297
* Set new state value.
9398
*

src/Mapper/DatabaseMapper.php

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -117,25 +117,22 @@ public function queueCreate($entity, Node $node, State $state): ContextCarrierIn
117117
*/
118118
public function queueUpdate($entity, Node $node, State $state): ContextCarrierInterface
119119
{
120+
$fromData = $state->getTransactionData();
120121
$data = $this->fetchFields($entity);
121122

122123
// in a future mapper must support solid states
123-
$changes = array_udiff_assoc($data, $state->getTransactionData(), [Node::class, 'compare']);
124-
$changedColumns = $this->mapColumns($changes);
125-
unset($changes[$this->primaryKey]);
126-
127-
$update = new Update($this->source->getDatabase(), $this->source->getTable(), $changedColumns);
124+
$changes = array_udiff_assoc($data, $fromData, [Node::class, 'compare']);
128125
$state->setStatus(Node::SCHEDULED_UPDATE);
129126
$state->setData($changes);
130127

131-
// we are trying to update entity without PK right now
132-
$state->forward(
133-
$this->primaryKey,
134-
$update,
135-
$this->primaryColumn,
136-
true,
137-
ConsumerInterface::SCOPE
138-
);
128+
$update = new Update($this->source->getDatabase(), $this->source->getTable(), $this->mapColumns($changes));
129+
if (isset($fromData[$this->primaryKey])) {
130+
// set update criteria right now
131+
$update->register($this->primaryColumn, $fromData[$this->primaryKey], false, ConsumerInterface::SCOPE);
132+
} else {
133+
// subscribe to PK update
134+
$state->forward($this->primaryKey, $update, $this->primaryColumn, true, ConsumerInterface::SCOPE);
135+
}
139136

140137
return $update;
141138
}

src/Transaction.php

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -147,17 +147,21 @@ protected function syncHeap(): void
147147
// optimize to only scan over affected entities
148148
$node = $heap->get($e);
149149

150+
if (!$node->hasState()) {
151+
continue;
152+
}
153+
150154
// marked as being deleted and has no external claims (GC like approach)
151-
if ($node->getStatus() == Node::SCHEDULED_DELETE && !$node->getState()->hasClaims()) {
155+
if ($node->getStatus() === Node::SCHEDULED_DELETE && !$node->getState()->hasClaims()) {
152156
$heap->detach($e);
153157
continue;
154158
}
155159

160+
// reindex the entity while it has old data
161+
$heap->attach($e, $node, $this->getIndexes($node->getRole()));
162+
156163
// sync the current entity data with newly generated data
157164
$this->orm->getMapper($node->getRole())->hydrate($e, $node->syncState());
158-
159-
// reindex the entity
160-
$heap->attach($e, $node, $this->getIndexes($node->getRole()));
161165
}
162166
}
163167

tests/ORM/Heap/HeapTest.php

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cycle\ORM\Tests\Heap;
6+
7+
use Cycle\ORM\Heap\Heap;
8+
use Cycle\ORM\Heap\HeapInterface;
9+
use Cycle\ORM\Heap\Node;
10+
use Cycle\ORM\Tests\Fixtures\User;
11+
use PHPUnit\Framework\TestCase;
12+
13+
class HeapTest extends TestCase
14+
{
15+
protected const
16+
INDEX_FIELDS_1 = 'id',
17+
INDEX_VALUES_1_1 = 42,
18+
INDEX_VALUES_1_2 = 24,
19+
INDEX_FIND_1_1 = [self::INDEX_FIELDS_1 => self::INDEX_VALUES_1_1],
20+
INDEX_FIND_1_2 = [self::INDEX_FIELDS_1 => self::INDEX_VALUES_1_2],
21+
INDEX_FIND_1_BAD = [self::INDEX_FIELDS_1 => 404],
22+
23+
INDEX_FIELDS_2 = 'email',
24+
INDEX_VALUES_2_1 = 'mail1@spiral',
25+
INDEX_VALUES_2_2 = 'mail2@spiral',
26+
INDEX_FIND_2_1 = [self::INDEX_FIELDS_2 => self::INDEX_VALUES_2_1],
27+
INDEX_FIND_2_2 = [self::INDEX_FIELDS_2 => self::INDEX_VALUES_2_2],
28+
INDEX_FIND_2_BAD = [self::INDEX_FIELDS_2 => 505],
29+
30+
ENTITY_SET_1 = [
31+
self::INDEX_FIELDS_1 => self::INDEX_VALUES_1_1,
32+
self::INDEX_FIELDS_2 => self::INDEX_VALUES_2_1,
33+
],
34+
ENTITY_SET_2 = [
35+
self::INDEX_FIELDS_1 => self::INDEX_VALUES_1_2,
36+
self::INDEX_FIELDS_2 => self::INDEX_VALUES_2_2,
37+
];
38+
39+
public function testAttachAndFind(): void
40+
{
41+
$heap = $this->createHeap();
42+
$node1 = new Node(Node::NEW, self::ENTITY_SET_1, 'user');
43+
$entity1 = new User();
44+
$node2 = new Node(Node::NEW, self::ENTITY_SET_2, 'user');
45+
$entity2 = new User();
46+
$heap->attach($entity1, $node1, [self::INDEX_FIELDS_1]);
47+
$heap->attach($entity2, $node2, [self::INDEX_FIELDS_1]);
48+
49+
$this->assertSame($entity2, $heap->find('user', self::INDEX_FIND_1_2), 'Found');
50+
$this->assertNull($heap->find('user', self::INDEX_FIND_1_BAD), 'Not found');
51+
$this->assertNull($heap->find('user', []), 'Empty scope');
52+
}
53+
54+
public function testDetach(): void
55+
{
56+
$heap = $this->createHeap();
57+
$node1 = new Node(Node::NEW, self::INDEX_FIND_1_1, 'user');
58+
$entity1 = new User();
59+
$node2 = new Node(Node::NEW, self::INDEX_FIND_1_2, 'user');
60+
$entity2 = new User();
61+
$heap->attach($entity1, $node1, [self::INDEX_FIELDS_1]);
62+
$heap->attach($entity2, $node2, [self::INDEX_FIELDS_1]);
63+
64+
$this->assertSame($entity1, $heap->find('user', self::INDEX_FIND_1_1), 'Found');
65+
$this->assertSame($entity2, $heap->find('user', self::INDEX_FIND_1_2), 'Found');
66+
$this->assertTrue($heap->has($entity1));
67+
$this->assertTrue($heap->has($entity2));
68+
69+
# Now detach it
70+
$heap->detach($entity1);
71+
$heap->detach($entity2);
72+
73+
$this->assertNull($heap->find('user', self::INDEX_FIND_1_1));
74+
$this->assertNull($heap->find('user', self::INDEX_FIND_1_2));
75+
$this->assertFalse($heap->has($entity1));
76+
$this->assertFalse($heap->has($entity2));
77+
}
78+
79+
public function testGet(): void
80+
{
81+
$heap = $this->createHeap();
82+
$node1 = new Node(Node::NEW, ['email' => 'test1'], 'user');
83+
$entity1 = new User();
84+
$node2 = new Node(Node::NEW, self::ENTITY_SET_2, 'user');
85+
$entity2 = new User();
86+
$heap->attach($entity1, $node1, [self::INDEX_FIELDS_1]);
87+
$heap->attach($entity2, $node2, [self::INDEX_FIELDS_1]);
88+
89+
$this->assertSame($node1, $heap->get($entity1));
90+
$this->assertSame($node2, $heap->get($entity2));
91+
}
92+
93+
public function testClean(): void
94+
{
95+
$heap = $this->createHeap();
96+
$node1 = new Node(Node::NEW, ['email' => 'test1'], 'user');
97+
$entity1 = new User();
98+
$node2 = new Node(Node::NEW, self::ENTITY_SET_1, 'user');
99+
$entity2 = new User();
100+
$heap->attach($entity1, $node1, [self::INDEX_FIELDS_1]);
101+
$heap->attach($entity2, $node2, [self::INDEX_FIELDS_1]);
102+
103+
# Now detach it
104+
$heap->clean();
105+
106+
$this->assertNull($heap->find('user', self::INDEX_FIND_1_1));
107+
$this->assertFalse($heap->has($entity1));
108+
$this->assertFalse($heap->has($entity2));
109+
110+
$count = 0;
111+
foreach ($heap as $value) {
112+
++$count;
113+
}
114+
$this->assertSame(0, $count);
115+
}
116+
117+
public function testSyncWhenIndexedValueChanged(): void
118+
{
119+
$heap = $this->createHeap();
120+
$node = new Node(Node::NEW, self::ENTITY_SET_1, 'user');
121+
$entity = new User();
122+
$heap->attach($entity, $node, [self::INDEX_FIELDS_1]);
123+
124+
foreach (self::ENTITY_SET_2 as $key => $value) {
125+
$node->getState()->register($key, $value);
126+
}
127+
$heap->attach($entity, $node, [self::INDEX_FIELDS_1]);
128+
129+
$this->assertNull($heap->find('user', self::INDEX_FIND_1_1));
130+
$this->assertSame($entity, $heap->find('user', self::INDEX_FIND_1_2));
131+
}
132+
133+
public function testSyncWhenEntitiesPKSwitch(): void
134+
{
135+
$heap = $this->createHeap();
136+
$node1 = new Node(Node::NEW, self::ENTITY_SET_1, 'user');
137+
$entity1 = new User();
138+
$node2 = new Node(Node::NEW, self::ENTITY_SET_2, 'user');
139+
$entity2 = new User();
140+
$heap->attach($entity1, $node1, [self::INDEX_FIELDS_1]);
141+
$heap->attach($entity2, $node2, [self::INDEX_FIELDS_1]);
142+
143+
foreach (self::ENTITY_SET_2 as $key => $value) {
144+
$node1->getState()->register($key, $value);
145+
}
146+
foreach (self::ENTITY_SET_1 as $key => $value) {
147+
$node2->getState()->register($key, $value);
148+
}
149+
$heap->attach($entity1, $node1, [self::INDEX_FIELDS_1]);
150+
$heap->attach($entity2, $node2, [self::INDEX_FIELDS_1]);
151+
152+
$this->assertSame($entity1, $heap->find('user', self::INDEX_FIND_1_2));
153+
$this->assertSame($entity2, $heap->find('user', self::INDEX_FIND_1_1));
154+
}
155+
156+
public function testOverwriteEntity(): void
157+
{
158+
$heap = $this->createHeap();
159+
$node1 = new Node(Node::NEW, self::ENTITY_SET_1, 'user');
160+
$entity1 = new User();
161+
$heap->attach($entity1, $node1, [self::INDEX_FIELDS_1]);
162+
163+
$node2 = new Node(Node::NEW, self::ENTITY_SET_1, 'user');
164+
$entity2 = new User();
165+
$heap->attach($entity2, $node2, [self::INDEX_FIELDS_1]);
166+
167+
$this->assertSame($entity2, $heap->find('user', self::INDEX_FIND_1_1));
168+
}
169+
170+
public function testAttachWithNewIndex(): void
171+
{
172+
$heap = $this->createHeap();
173+
$node = new Node(Node::NEW, self::ENTITY_SET_1, 'user');
174+
$entity = new User();
175+
$heap->attach($entity, $node, [self::INDEX_FIELDS_1]);
176+
177+
$this->assertNull($heap->find('user', self::INDEX_FIND_2_1));
178+
179+
$heap->attach($entity, $node, [self::INDEX_FIELDS_2]);
180+
181+
$this->assertSame($entity, $heap->find('user', self::INDEX_FIND_2_1));
182+
// old index was not deleted
183+
$this->assertSame($entity, $heap->find('user', self::INDEX_FIND_1_1));
184+
}
185+
186+
public function testSyncWhenIndexAndValuesChanged(): void
187+
{
188+
$heap = $this->createHeap();
189+
$node = new Node(Node::NEW, self::ENTITY_SET_1, 'user');
190+
$entity = new User();
191+
$heap->attach($entity, $node, [self::INDEX_FIELDS_1]);
192+
193+
foreach (self::ENTITY_SET_2 as $key => $value) {
194+
$node->getState()->register($key, $value);
195+
}
196+
$heap->attach($entity, $node, [self::INDEX_FIELDS_2]);
197+
198+
$this->assertNull($heap->find('user', self::INDEX_FIND_1_1));
199+
$this->assertNull($heap->find('user', self::INDEX_FIND_1_2));
200+
$this->assertNull($heap->find('user', self::INDEX_FIND_2_1));
201+
$this->assertSame($entity, $heap->find('user', self::INDEX_FIND_2_2));
202+
}
203+
204+
protected function createHeap(): HeapInterface
205+
{
206+
return new Heap();
207+
}
208+
}

tests/ORM/RenamedPKTest.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@ public function testChangePK(): void
6969
$this->save($u);
7070
$this->assertNumWrites(1);
7171

72-
$this->orm = $this->orm->withHeap(new Heap());
7372
$data = (new Select($this->orm, Identity::class))
7473
->fetchAll();
7574

0 commit comments

Comments
 (0)