depage-xmldb v2.3.1
XML documents in a relational database
Loading...
Searching...
No Matches
Positions.php
Go to the documentation of this file.
1<?php
2
12
13namespace Depage\XmlDb\Helpers;
14
15use Depage\XmlDb\Exceptions\XmlDbException;
16
36{
40 public const POS_GAP = 65536;
41
46 public const POS_MIN_GAP = 16;
47
55 public const SCRATCH_OFFSET = 1125899906842624;
56
57 protected $pdo;
58
66 public function __construct(
67 $pdo,
68 protected string $table,
69 protected $docId,
70 protected int $batchSize = CaseUpdate::DEFAULT_SIZE
71 ) {
72 $this->pdo = $pdo;
73 }
74
82 public function siblings($parentId): array
83 {
84 $query = $this->pdo->prepare(
85 "SELECT xml.id, xml.pos
86 FROM {$this->table} AS xml
87 WHERE xml.id_parent = :parent_id AND xml.id_doc = :doc_id
88 ORDER BY xml.pos ASC, xml.id ASC",
89 );
90 $query->execute([
91 'parent_id' => $parentId,
92 'doc_id' => $this->docId,
93 ]);
94
95 return $query->fetchAll(\PDO::FETCH_OBJ);
96 }
97
108 private function siblingsWithout($parentId, $excludeId = null): array
109 {
110 $siblings = $this->siblings($parentId);
111
112 if (!is_null($excludeId)) {
113 foreach ($siblings as $i => $sibling) {
114 if ($sibling->id == $excludeId) {
115 unset($siblings[$i]);
116 $siblings = array_values($siblings);
117 break;
118 }
119 }
120 }
121
122 return $siblings;
123 }
124
139 private function clampIndex($targetIndex, int $numSiblings): int
140 {
141 if (!is_numeric($targetIndex) || (int) $targetIndex < 0) {
142 return $numSiblings;
143 }
144
145 return min((int) $targetIndex, $numSiblings);
146 }
147
161 public function normalizeIndex($parentId, $targetIndex, $excludeId = null)
162 {
163 return $this->clampIndex(
164 $targetIndex,
165 count($this->siblingsWithout($parentId, $excludeId))
166 );
167 }
168
179 public function indexById($id)
180 {
181 $query = $this->pdo->prepare(
182 "SELECT xml.id_parent, xml.pos
183 FROM {$this->table} AS xml
184 WHERE xml.id = :id AND xml.id_doc = :doc_id
185 LIMIT 1",
186 );
187 $query->execute([
188 'id' => $id,
189 'doc_id' => $this->docId,
190 ]);
191
192 if (!($node = $query->fetchObject())) {
193 return false;
194 }
195
196 $query = $this->pdo->prepare(
197 "SELECT COUNT(*) AS sibling_index
198 FROM {$this->table} AS xml
199 WHERE xml.id_parent = :parent_id AND xml.id_doc = :doc_id AND xml.pos < :pos",
200 );
201 $query->execute([
202 'parent_id' => $node->id_parent,
203 'pos' => $node->pos,
204 'doc_id' => $this->docId,
205 ]);
206 $result = $query->fetchObject();
207
208 return (int) $result->sibling_index;
209 }
210
217 public function appendIndex($parentId)
218 {
219 $query = $this->pdo->prepare(
220 "SELECT COUNT(*) AS pos
221 FROM {$this->table} AS xml
222 WHERE xml.id_parent = :target_id AND id_doc = :doc_id",
223 );
224 $query->execute([
225 'target_id' => $parentId,
226 'doc_id' => $this->docId,
227 ]);
228
229 if ($object = $query->fetchObject()) {
230 $result = $object->pos;
231 } else {
232 $result = null;
233 }
234
235 return $result;
236 }
237
253 public function positionForIndex($parentId, $index, $excludeId = null)
254 {
255 $siblings = $this->siblingsWithout($parentId, $excludeId);
256
257 $numSiblings = count($siblings);
258 $index = $this->clampIndex($index, $numSiblings);
259
260 if ($index >= $numSiblings) {
261 // appending does not need room between two neighbours
262 $lastPos = $numSiblings > 0 ? (int) $siblings[$numSiblings - 1]->pos : 0;
263
264 return $lastPos + self::POS_GAP;
265 }
266
267 $prevPos = $index > 0 ? (int) $siblings[$index - 1]->pos : 0;
268 $nextPos = (int) $siblings[$index]->pos;
269
270 if ($nextPos - $prevPos - 1 < self::POS_MIN_GAP) {
271 $this->rebalance($parentId);
272
273 return $this->positionForIndex($parentId, $index, $excludeId);
274 }
275
276 return intdiv($prevPos + $nextPos, 2);
277 }
278
288 public function rebalance($parentId, $order = null): void
289 {
290 $siblings = $this->siblings($parentId);
291
292 if (count($siblings) == 0) {
293 return;
294 }
295
296 $current = [];
297 foreach ($siblings as $sibling) {
298 $current[] = (int) $sibling->id;
299 }
300
301 if (is_null($order)) {
302 // keep the order that is stored in the database
303 $ids = $current;
304 } else {
305 // reorder into the given child order (used by the patch
306 // applier)
307 $ids = array_map('intval', $order);
308
309 if (count($ids) != count($current) || array_diff($ids, $current) || array_diff($current, $ids)) {
310 throw new XmlDbException("Can not reorder the children of node {$parentId}, the node list does not match.");
311 }
312 }
313
314 // the unique sibling index is checked row by row, so the positions
315 // have to be moved out of the way first: adding a big offset in
316 // descending order cannot collide with a sibling that has not
317 // been updated yet, and the offset is big enough to keep the
318 // scratch positions clear of the final positions that are
319 // assigned in the update below
320 $this->pdo->exec(
321 "UPDATE {$this->table}
322 SET pos = pos + " . self::SCRATCH_OFFSET . "
323 WHERE id_doc = " . (int) $this->docId . " AND id_parent = " . (int) $parentId . "
324 ORDER BY pos DESC"
325 );
326
327 // every sibling gets a position of its own, so writing the final
328 // positions in batches cannot collide with the unique sibling
329 // index either
330 $positions = new CaseUpdate($this->pdo, $this->table, $this->batchSize);
331 foreach ($ids as $index => $id) {
332 $positions->add($id, ($index + 1) * self::POS_GAP);
333 }
334 $positions->write('pos', ['id_doc' => $this->docId]);
335 }
336}
337
338/* vim:set ft=php sw=4 sts=4 fdm=marker et : */
const DEFAULT_SIZE
number of rows written with a single statement
normalizeIndex($parentId, $targetIndex, $excludeId=null)
gets the index a node is inserted at in the child list of a parent node for the targetIndex a caller ...
indexById($id)
gets the index of a node in the child list of its parent node (number of siblings before it),...
const POS_MIN_GAP
the child list is rebalanced once the space between two neighbours drops below this gap
Definition Positions.php:46
const POS_GAP
distance between the sparse positions of sibling nodes
Definition Positions.php:40
const SCRATCH_OFFSET
adding this offset to all positions of a child list (in descending order, before assigning the final ...
Definition Positions.php:55
__construct( $pdo, protected string $table, protected $docId, protected int $batchSize=CaseUpdate::DEFAULT_SIZE)
Definition Positions.php:66
positionForIndex($parentId, $index, $excludeId=null)
calculates the sparse pos value for a node to be inserted at the given index of a parent node's child...
siblings($parentId)
gets id and pos of all children of a parent node in document order
Definition Positions.php:82
appendIndex($parentId)
gets the index a child would get when appended to the child list, i.e.
rebalance($parentId, $order=null)
renumbers the children of a parent node to have an even distance of self\POS_GAP between their positi...