PluginProbe
404 Solution / trunk
404 Solution vtrunk
4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.2.0 4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.1.7 4.1.6 4.1.5 4.1.4 4.1.3 trunk 2.30.0 All 109 releases
404-solution / includes / database / IndexDefinitionComparator.php

IndexDefinitionComparator.php in 404 Solution trunk, at includes/database/IndexDefinitionComparator.php

149 lines 6.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7 /**
8 * Whether a live index and the DDL that declares it are the same index, and the
9 * canonical comparable form both reduce to in order to answer that.
10 *
11 * This is the third of three modules that together own the index picture, and
12 * the only one that sees both halves:
13 *
14 * - {@see ABJ_404_Solution_CreateTableIndexParser} turns DDL SOURCE TEXT (the
15 * plugin's own create*Table.sql templates) into specs. Regex over SQL.
16 * - {@see ABJ_404_Solution_TableIndexDefinitions} reads the LIVE ENGINE
17 * (SHOW INDEX) and normalizes whatever the driver reported into definitions.
18 * - this module reduces either representation to one signature string and
19 * compares them.
20 *
21 * Splitting the comparison out from the reader is what lets the reader stop
22 * knowing that the DDL parser exists at all: the dependency runs one way, from
23 * here down to both producers, and never back.
24 *
25 * THE CONTRACT, and the reason every method here can answer "no":
26 *
27 * An index that was not fully described has NO signature. Not an empty one, not
28 * a placeholder -- none. "n:" looks like a signature and compares like one, but
29 * it stands for an index with no columns, which cannot exist, so it compares
30 * unequal to every real definition and the repair path reads that as drift. The
31 * same is true of a live definition the engine described incompletely. Both
32 * mean "cannot describe this index", and both keep meaning that all the way to
33 * the caller.
34 *
35 * {@see isDriftedFromDdlSpec()} is where that meets the callers. It is
36 * deliberately two-valued over a three-valued reality, because the response to
37 * "differs" is a DROP INDEX plus ADD INDEX over the whole table (and, for the
38 * spelling cache, emptying it first) while the response to "agrees" is nothing
39 * at all. "Could not be established" therefore belongs with "agrees".
40 *
41 * Everything here is pure: two in-memory shapes in, a string or a verdict out.
42 * No queries, no DDL, no formatting for humans.
43 */
44 class ABJ_404_Solution_IndexDefinitionComparator {
45
46 /**
47 * A canonical, comparable string for one index definition, or NULL when the
48 * column list does not describe an index that could exist.
49 *
50 * Two definitions are the same index exactly when their signatures match:
51 * same uniqueness, same columns, in the same order, with the same prefix
52 * lengths. Example: "n:status,disabled,logshits,id" or "n:url(190),disabled".
53 *
54 * An empty list reaches here from both sides: ddlColumnList() answers a
55 * fragment it only partly understands with an empty list, and a live
56 * definition assembled from rows this version could not read carries one
57 * too. An entry with no readable column name voids the signature rather
58 * than being skipped, for the same reason the DDL parser refuses a partial
59 * parse: skipping produces a SHORTER list that reads as a complete
60 * definition, and repairing a real index to that shorter shape is how a
61 * read gap turns into deliberate data-structure damage.
62 *
63 * @param array<int, array{column: string, prefix: int|null}> $columnList
64 * @param bool $unique
65 * @return string|null
66 */
67 public static function signature(array $columnList, bool $unique): ?string {
68 if (empty($columnList)) {
69 return null;
70 }
71 $parts = array();
72 foreach ($columnList as $column) {
73 $name = isset($column['column']) && is_scalar($column['column'])
74 ? strtolower(trim((string)$column['column'])) : '';
75 if ($name === '') {
76 return null;
77 }
78 $prefix = isset($column['prefix']) ? '(' . (int)$column['prefix'] . ')' : '';
79 $parts[] = $name . $prefix;
80 }
81 return ($unique ? 'u:' : 'n:') . implode(',', $parts);
82 }
83
84 /**
85 * The signature of a DDL spec produced by
86 * {@see ABJ_404_Solution_CreateTableIndexParser::fromCreateTableSql()}, or
87 * NULL when that spec's column fragment did not parse -- a truncated or
88 * corrupted create*Table.sql, which defensive philosophy #7 says to expect.
89 *
90 * @param array{name: string, columns: string, unique: bool} $spec
91 * @return string|null
92 */
93 public static function signatureOfDdlSpec(array $spec): ?string {
94 return self::signature(
95 ABJ_404_Solution_CreateTableIndexParser::ddlColumnList(
96 isset($spec['columns']) ? (string)$spec['columns'] : ''),
97 !empty($spec['unique'])
98 );
99 }
100
101 /**
102 * The signature of a live definition produced by
103 * {@see ABJ_404_Solution_TableIndexDefinitions::readLive()}, or NULL when
104 * the engine's description of it was incomplete.
105 *
106 * The describability check is repeated here rather than left to the caller
107 * on purpose: a call site that forgets it gets a comparable string for an
108 * index nobody described, and the only thing it can do with the resulting
109 * mismatch is rewrite the table. Making the undescribable case impossible to
110 * ask a comparable question about is cheaper than auditing every future
111 * caller for the gate.
112 *
113 * @param array{name?: string, columns?: array<int, array{column: string, prefix: int|null}>, unique?: bool, describable?: bool} $definition
114 * @return string|null
115 */
116 public static function signatureOfLiveDefinition(array $definition): ?string {
117 if (!ABJ_404_Solution_TableIndexDefinitions::isDescribable($definition)) {
118 return null;
119 }
120 $columns = isset($definition['columns']) && is_array($definition['columns'])
121 ? $definition['columns'] : array();
122 return self::signature($columns, !empty($definition['unique']));
123 }
124
125 /**
126 * Whether a live index has been ESTABLISHED to differ from the DDL that
127 * declares it.
128 *
129 * The one question the repair path acts on. Collapsing "could not be
130 * established" into "not drifted" happens here, once, where the two
131 * signatures are read, so that no caller has to re-derive that null is not
132 * a difference. Comparing the signatures directly is exactly the mistake
133 * this method exists to prevent: PHP reports null !== 'n:url(190)' as a
134 * difference, and that difference is a table rewrite.
135 *
136 * @param array{columns?: array<int, array{column: string, prefix: int|null}>, unique?: bool, describable?: bool} $liveDefinition
137 * @param array{name: string, columns: string, unique: bool} $ddlSpec
138 * @return bool
139 */
140 public static function isDriftedFromDdlSpec(array $liveDefinition, array $ddlSpec): bool {
141 $liveSignature = self::signatureOfLiveDefinition($liveDefinition);
142 $goalSignature = self::signatureOfDdlSpec($ddlSpec);
143 if ($liveSignature === null || $goalSignature === null) {
144 return false;
145 }
146 return $liveSignature !== $goalSignature;
147 }
148 }
149