Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
97.14% |
34 / 35 |
|
80.00% |
4 / 5 |
CRAP | |
0.00% |
0 / 1 |
| Indexes | |
97.14% |
34 / 35 |
|
80.00% |
4 / 5 |
14 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| reconcile | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| update | |
96.77% |
30 / 31 |
|
0.00% |
0 / 1 |
10 | |||
| existing | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| signature | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Cosray\Title; |
| 6 | |
| 7 | use Celema\Quma\Database; |
| 8 | use Cosray\Exception\RuntimeException; |
| 9 | use Cosray\Locales; |
| 10 | use Cosray\Util\Transaction; |
| 11 | |
| 12 | /** Reconciles only the title indexes on the configured nodes table. */ |
| 13 | final class Indexes |
| 14 | { |
| 15 | public function __construct( |
| 16 | private readonly Database $db, |
| 17 | private readonly Locales $locales, |
| 18 | ) {} |
| 19 | |
| 20 | /** @return array{created: int, dropped: int, locales: list<string>} */ |
| 21 | public function reconcile(): array |
| 22 | { |
| 23 | return Transaction::run($this->db, $this->update(...)); |
| 24 | } |
| 25 | |
| 26 | /** @return array{created: int, dropped: int, locales: list<string>} */ |
| 27 | private function update(): array |
| 28 | { |
| 29 | $sort = new Sort($this->db); |
| 30 | $wanted = []; |
| 31 | foreach ($this->locales as $locale) { |
| 32 | $name = Sort::indexName($locale->id); |
| 33 | if (strlen($name) > 63 || isset($wanted[$name])) { |
| 34 | throw new RuntimeException("Title sort index name is too long or ambiguous: '{$name}'"); |
| 35 | } |
| 36 | $wanted[$name] = ['locale' => $locale->id, 'expression' => $sort->order($locale)]; |
| 37 | } |
| 38 | |
| 39 | $existing = $this->existing(); |
| 40 | $created = 0; |
| 41 | $dropped = 0; |
| 42 | foreach ($wanted as $name => $definition) { |
| 43 | $source = hash('sha256', $definition['expression']); |
| 44 | $current = $existing[$name] ?? null; |
| 45 | if ( |
| 46 | $current !== null |
| 47 | && $current['valid'] |
| 48 | && $current['signature'] === $this->signature($source, $current['definition']) |
| 49 | ) { |
| 50 | continue; |
| 51 | } |
| 52 | if ($current !== null) { |
| 53 | $this->db->titleSort->drop(['index' => $current['identifier']])->run(); |
| 54 | $dropped++; |
| 55 | } |
| 56 | $this->db->titleSort->create(['index' => $name, 'expression' => $definition['expression']])->run(); |
| 57 | $current = $this->existing()[$name]; |
| 58 | $this->db->titleSort->mark([ |
| 59 | 'index' => $current['identifier'], |
| 60 | // COMMENT cannot bind a parameter. Both hashes are generated here. |
| 61 | 'signature' => $this->db->quote($this->signature($source, $current['definition'])), |
| 62 | ])->run(); |
| 63 | $created++; |
| 64 | } |
| 65 | |
| 66 | foreach (array_diff_key($existing, $wanted) as $index) { |
| 67 | $this->db->titleSort->drop(['index' => $index['identifier']])->run(); |
| 68 | $dropped++; |
| 69 | } |
| 70 | |
| 71 | return ['created' => $created, 'dropped' => $dropped, 'locales' => array_column($wanted, 'locale')]; |
| 72 | } |
| 73 | |
| 74 | /** @return array<array<mixed>> */ |
| 75 | private function existing(): array |
| 76 | { |
| 77 | return array_column($this->db->titleSort->indexes()->all(), null, 'name'); |
| 78 | } |
| 79 | |
| 80 | private function signature(string $source, string $definition): string |
| 81 | { |
| 82 | // Include PostgreSQL's actual definition so manual replacements are |
| 83 | // detected without attempting to normalize its expression deparser. |
| 84 | return 'cosray-title-sort:' . $source . ':' . hash('sha256', $definition); |
| 85 | } |
| 86 | } |