PluginProbe
404 Solution / 4.3.3
404 Solution v4.3.3
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 / CreateTableIndexParser.php

CreateTableIndexParser.php in 404 Solution 4.3.3, at includes/database/CreateTableIndexParser.php

139 lines 5.7 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 * Parses the KEY / UNIQUE KEY declarations out of the plugin's own
9 * create*Table.sql templates into structured index specs.
10 *
11 * This is the DDL-source half of the index picture: pure text in, structure
12 * out, with no database connection and no knowledge of what any engine
13 * currently reports. The live-metadata half, and the comparison that decides
14 * whether the two agree, belong to
15 * {@see ABJ_404_Solution_TableIndexDefinitions}, which is where they can be
16 * compared as one operation.
17 *
18 * The parser's governing rule, and the reason it refuses rather than guesses:
19 * a fragment it only PARTLY understands must yield nothing at all. Scraping the
20 * recognisable names out of an unrecognised fragment produces a SHORTER column
21 * list that looks like a complete definition, and the repair path would then
22 * rebuild a real index to that shorter shape -- turning a parse gap into
23 * deliberate data-structure damage. Every caller already reads an empty result
24 * as "cannot describe this index", never as "this index has no columns".
25 */
26 class ABJ_404_Solution_CreateTableIndexParser {
27
28 /**
29 * Extract index specs from a CREATE TABLE statement (plugin SQL templates),
30 * keyed by index name exactly as the DDL spells it.
31 *
32 * Only plain KEY / UNIQUE KEY definitions are recognised. FULLTEXT and
33 * SPATIAL keys are deliberately not matched: the plugin ships none, and
34 * silently mis-parsing one into a plain key would let the repair path
35 * rebuild it as the wrong kind of index.
36 *
37 * @param string $createTableSql
38 * @return array<string, array{name: string, columns: string, unique: bool}>
39 */
40 public static function fromCreateTableSql($createTableSql): array {
41 if (!is_string($createTableSql) || $createTableSql === '') {
42 return array();
43 }
44
45 $matches = array();
46 preg_match_all('/^\\s*(?:unique\\s+)?key\\s+.+?\\s*$/im', $createTableSql, $matches);
47
48 $specsByName = array();
49 foreach ($matches[0] as $line) {
50 $spec = self::parseIndexDdlLine($line);
51 if (empty($spec) || empty($spec['name'])) {
52 continue;
53 }
54 $specsByName[$spec['name']] = $spec;
55 }
56
57 return $specsByName;
58 }
59
60 /**
61 * Parse one index DDL line from our CREATE TABLE SQL into a structured spec.
62 *
63 * Accepts forms like:
64 * - KEY `name` (`col`(190), `other`)
65 * - UNIQUE KEY `name` (`col`)
66 * - KEY `name` (`col`) USING BTREE
67 *
68 * Returns null if the line doesn't look like a KEY/UNIQUE KEY definition.
69 *
70 * @param string $indexDDL
71 * @return array{name: string, columns: string, unique: bool}|null
72 */
73 public static function parseIndexDdlLine($indexDDL) {
74 $indexDDL = trim((string)$indexDDL);
75 // Tolerate a trailing comma -- the line-extracting regex pulls each
76 // KEY definition out as-is from the surrounding CREATE TABLE list,
77 // and any KEY that isn't the LAST one will end with a comma. Same
78 // canonical form either way.
79 $indexDDL = rtrim($indexDDL, ',');
80 $matches = array();
81 if (!preg_match('/^(unique\\s+)?key\\s+`?([^`\\s]+)`?\\s*(\\(.+\\))\\s*(?:using\\s+\\w+)?\\s*$/i', $indexDDL, $matches)) {
82 return null;
83 }
84
85 return array(
86 'name' => $matches[2],
87 'columns' => $matches[3],
88 'unique' => !empty($matches[1]),
89 );
90 }
91
92 /**
93 * The ordered (column, prefix) list a DDL column fragment describes.
94 *
95 * Input is the parenthesised fragment a spec carries, e.g.
96 * "(`status`, `disabled`, `logshits`, `id`)" or "(`url`(190), `disabled`)".
97 * Backticks are required (every shipped create*Table.sql uses them, and
98 * DDLColumnParsingRobustnessTest enforces it), so a fragment written some
99 * other way yields an empty list -- which the repair path treats as
100 * "cannot describe this index", never as "this index has no columns".
101 *
102 * @param string $columnsSql
103 * @return array<int, array{column: string, prefix: int|null}>
104 */
105 public static function ddlColumnList($columnsSql): array {
106 $fragment = trim((string)$columnsSql);
107 $columns = array();
108 $matches = array();
109 preg_match_all('/`([^`]+)`\\s*(?:\\(\\s*(\\d+)\\s*\\))?/', $fragment, $matches,
110 PREG_SET_ORDER);
111 foreach ($matches as $match) {
112 // A (0) prefix and no prefix at all are the same physical index, and
113 // the live reader already reports a Sub_part of 0 as "no prefix".
114 // Spelling it `url(0)` here while the live side spells it `url`
115 // makes two descriptions of one index compare as drift, and the
116 // repair path answers drift by rewriting the table. MySQL rejects a
117 // zero-length key part, so no create*Table.sql the plugin ships can
118 // reach this -- but the two sides of a comparison agreeing about
119 // what a value MEANS should not rest on the value never occurring.
120 $prefix = isset($match[2]) ? (int)$match[2] : null;
121 $columns[] = array(
122 'column' => strtolower($match[1]),
123 'prefix' => ($prefix === null || $prefix <= 0) ? null : $prefix,
124 );
125 }
126
127 // Verify the whole fragment was accounted for, not just the parts that
128 // happened to match. See the class comment: a partial parse that looks
129 // complete is worse than no parse at all.
130 $remainder = preg_replace('/`[^`]+`\\s*(?:\\(\\s*\\d+\\s*\\))?/', '', $fragment);
131 $remainder = trim((string)$remainder, " \t\n\r\0\x0B(),");
132 if ($remainder !== '') {
133 return array();
134 }
135
136 return $columns;
137 }
138 }
139