Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
97.01% |
65 / 67 |
|
91.67% |
11 / 12 |
CRAP | |
0.00% |
0 / 1 |
DatabaseDomain | |
97.01% |
65 / 67 |
|
91.67% |
11 / 12 |
43 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
10 | |||
newFromId | |
91.30% |
21 / 23 |
|
0.00% |
0 / 1 |
9.05 | |||
newUnspecified | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
equals | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
isCompatible | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
6 | |||
isUnspecified | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
getDatabase | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getSchema | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getTablePrefix | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getId | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
convertToString | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
5 | |||
__toString | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | namespace Wikimedia\Rdbms; |
21 | |
22 | use InvalidArgumentException; |
23 | use Stringable; |
24 | |
25 | /** |
26 | * Class to handle database/schema/prefix specifications for IDatabase |
27 | * |
28 | * The components of a database domain are defined as follows: |
29 | * - database: name of a server-side collection of schemas that is client-selectable |
30 | * - schema: name of a server-side collection of tables within the given database |
31 | * - prefix: table name prefix of an application-defined table collection |
32 | * |
33 | * If an RDBMS does not support server-side collections of table collections (schemas) then |
34 | * the schema component should be null and the "database" component treated as a collection |
35 | * of exactly one table collection (the implied schema for that "database"). |
36 | * |
37 | * The above criteria should determine how components should map to RDBMS specific keywords |
38 | * rather than "database"/"schema" always mapping to "DATABASE"/"SCHEMA" as used by the RDBMS. |
39 | * |
40 | * @ingroup Database |
41 | */ |
42 | class DatabaseDomain implements Stringable { |
43 | /** @var string|null */ |
44 | private $database; |
45 | /** @var string|null */ |
46 | private $schema; |
47 | /** @var string */ |
48 | private $prefix; |
49 | |
50 | /** @var string Cache of convertToString() */ |
51 | private $equivalentString; |
52 | |
53 | /** |
54 | * @param string|null $database Database name |
55 | * @param string|null $schema Schema name |
56 | * @param string $prefix Table prefix |
57 | */ |
58 | public function __construct( $database, $schema, $prefix ) { |
59 | if ( $database !== null && ( !is_string( $database ) || $database === '' ) ) { |
60 | throw new InvalidArgumentException( 'Database must be null or a non-empty string.' ); |
61 | } |
62 | $this->database = $database; |
63 | if ( $schema !== null && ( !is_string( $schema ) || $schema === '' ) ) { |
64 | throw new InvalidArgumentException( 'Schema must be null or a non-empty string.' ); |
65 | } elseif ( $database === null && $schema !== null ) { |
66 | throw new InvalidArgumentException( 'Schema must be null if database is null.' ); |
67 | } |
68 | $this->schema = $schema; |
69 | if ( !is_string( $prefix ) ) { |
70 | throw new InvalidArgumentException( "Prefix must be a string." ); |
71 | } |
72 | $this->prefix = $prefix; |
73 | } |
74 | |
75 | /** |
76 | * @param DatabaseDomain|string $domain Result of DatabaseDomain::toString() |
77 | * @return DatabaseDomain |
78 | */ |
79 | public static function newFromId( $domain ): self { |
80 | if ( $domain instanceof self ) { |
81 | return $domain; |
82 | } |
83 | |
84 | if ( !is_string( $domain ) ) { |
85 | throw new InvalidArgumentException( "Domain must be a string or " . __CLASS__ ); |
86 | } |
87 | |
88 | $parts = explode( '-', $domain ); |
89 | foreach ( $parts as &$part ) { |
90 | $part = strtr( $part, [ '?h' => '-', '??' => '?' ] ); |
91 | } |
92 | |
93 | $schema = null; |
94 | $prefix = ''; |
95 | |
96 | if ( count( $parts ) == 1 ) { |
97 | $database = $parts[0]; |
98 | } elseif ( count( $parts ) == 2 ) { |
99 | [ $database, $prefix ] = $parts; |
100 | } elseif ( count( $parts ) == 3 ) { |
101 | [ $database, $schema, $prefix ] = $parts; |
102 | } else { |
103 | throw new InvalidArgumentException( "Domain '$domain' has too few or too many parts." ); |
104 | } |
105 | |
106 | if ( $database === '' ) { |
107 | $database = null; |
108 | } |
109 | |
110 | if ( $schema === '' ) { |
111 | $schema = null; |
112 | } |
113 | |
114 | $instance = new self( $database, $schema, $prefix ); |
115 | $instance->equivalentString = $domain; |
116 | |
117 | return $instance; |
118 | } |
119 | |
120 | /** |
121 | * @return DatabaseDomain |
122 | */ |
123 | public static function newUnspecified() { |
124 | return new self( null, null, '' ); |
125 | } |
126 | |
127 | /** |
128 | * @param DatabaseDomain|string $other |
129 | * @return bool Whether the domain instances are the same by value |
130 | */ |
131 | public function equals( $other ) { |
132 | if ( $other instanceof self ) { |
133 | return ( |
134 | $this->database === $other->database && |
135 | $this->schema === $other->schema && |
136 | $this->prefix === $other->prefix |
137 | ); |
138 | } |
139 | |
140 | return ( $this->getId() === $other ); |
141 | } |
142 | |
143 | /** |
144 | * Check whether the domain $other meets the specifications of this domain |
145 | * |
146 | * If this instance has a null database specifier, then $other can have any database |
147 | * specifier, including null. This is likewise true if the schema specifier is null. |
148 | * This is not transitive like equals() since a domain that explicitly wants a certain |
149 | * database or schema cannot be satisfied by one of another (nor null). If the prefix |
150 | * is empty and the DB and schema are both null, then the entire domain is considered |
151 | * unspecified, and any prefix of $other is considered compatible. |
152 | * |
153 | * @param DatabaseDomain|string $other |
154 | * @return bool |
155 | * @since 1.32 |
156 | */ |
157 | public function isCompatible( $other ) { |
158 | if ( $this->isUnspecified() ) { |
159 | return true; // even the prefix doesn't matter |
160 | } |
161 | |
162 | $other = self::newFromId( $other ); |
163 | |
164 | return ( |
165 | ( $this->database === $other->database || $this->database === null ) && |
166 | ( $this->schema === $other->schema || $this->schema === null ) && |
167 | $this->prefix === $other->prefix |
168 | ); |
169 | } |
170 | |
171 | /** |
172 | * @return bool |
173 | * @since 1.32 |
174 | */ |
175 | public function isUnspecified() { |
176 | return ( |
177 | $this->database === null && $this->schema === null && $this->prefix === '' |
178 | ); |
179 | } |
180 | |
181 | /** |
182 | * @return string|null Database name |
183 | */ |
184 | public function getDatabase() { |
185 | return $this->database; |
186 | } |
187 | |
188 | /** |
189 | * @return string|null Database schema |
190 | */ |
191 | public function getSchema() { |
192 | return $this->schema; |
193 | } |
194 | |
195 | /** |
196 | * @return string Table prefix |
197 | */ |
198 | public function getTablePrefix() { |
199 | return $this->prefix; |
200 | } |
201 | |
202 | /** |
203 | * @return string |
204 | */ |
205 | public function getId(): string { |
206 | $this->equivalentString ??= $this->convertToString(); |
207 | |
208 | return $this->equivalentString; |
209 | } |
210 | |
211 | /** |
212 | * @return string |
213 | */ |
214 | private function convertToString(): string { |
215 | $parts = [ (string)$this->database ]; |
216 | if ( $this->schema !== null ) { |
217 | $parts[] = $this->schema; |
218 | } |
219 | if ( $this->prefix != '' || $this->schema !== null ) { |
220 | // If there is a schema, then we need the prefix to disambiguate. |
221 | // For engines like Postgres that use schemas, this awkwardness is hopefully |
222 | // avoided since it is easy to have one DB per server (to avoid having many users) |
223 | // and use schema/prefix to have wiki farms. For example, a domain schemes could be |
224 | // wiki-<project>-<language>, e.g. "wiki-fitness-es"/"wiki-sports-fr"/"wiki-news-en". |
225 | $parts[] = $this->prefix; |
226 | } |
227 | |
228 | foreach ( $parts as &$part ) { |
229 | $part = strtr( $part, [ '-' => '?h', '?' => '??' ] ); |
230 | } |
231 | return implode( '-', $parts ); |
232 | } |
233 | |
234 | /** |
235 | * @return string |
236 | */ |
237 | public function __toString() { |
238 | return $this->getId(); |
239 | } |
240 | } |