Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
72.09% |
62 / 86 |
|
16.67% |
1 / 6 |
CRAP | |
0.00% |
0 / 1 |
| DatabaseFactory | |
72.09% |
62 / 86 |
|
16.67% |
1 / 6 |
30.58 | |
0.00% |
0 / 1 |
| __construct | |
88.89% |
8 / 9 |
|
0.00% |
0 / 1 |
2.01 | |||
| create | |
89.19% |
33 / 37 |
|
0.00% |
0 / 1 |
5.03 | |||
| attributesFromType | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
| getClass | |
72.00% |
18 / 25 |
|
0.00% |
0 / 1 |
9.40 | |||
| initConnectionFlags | |
40.00% |
2 / 5 |
|
0.00% |
0 / 1 |
4.94 | |||
| fieldHasBit | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * @license GPL-2.0-or-later |
| 4 | * @file |
| 5 | */ |
| 6 | namespace Wikimedia\Rdbms; |
| 7 | |
| 8 | use InvalidArgumentException; |
| 9 | use Psr\Log\NullLogger; |
| 10 | use Throwable; |
| 11 | use Wikimedia\ObjectCache\HashBagOStuff; |
| 12 | use Wikimedia\RequestTimeout\CriticalSectionProvider; |
| 13 | use Wikimedia\Telemetry\NoopTracer; |
| 14 | use Wikimedia\Telemetry\TracerInterface; |
| 15 | |
| 16 | /** |
| 17 | * Constructs Database objects |
| 18 | * |
| 19 | * @since 1.39 |
| 20 | * @ingroup Database |
| 21 | */ |
| 22 | class DatabaseFactory { |
| 23 | /** @var string Agent name for query profiling */ |
| 24 | private $agent; |
| 25 | /** @var callable Deprecation logger */ |
| 26 | private $deprecationLogger; |
| 27 | /** |
| 28 | * @var callable|null An optional callback that returns a ScopedCallback instance, |
| 29 | * meant to profile the actual query execution in {@see Database::doQuery} |
| 30 | */ |
| 31 | private $profiler; |
| 32 | /** @var TracerInterface */ |
| 33 | private $tracer; |
| 34 | /** @var CriticalSectionProvider|null */ |
| 35 | private $csProvider; |
| 36 | /** @var bool Whether this PHP instance is for a CLI script */ |
| 37 | private $cliMode; |
| 38 | /** @var bool Log SQL queries in debug toolbar if set to true */ |
| 39 | private $debugSql; |
| 40 | |
| 41 | public function __construct( array $params = [] ) { |
| 42 | $this->agent = $params['agent'] ?? ''; |
| 43 | $this->deprecationLogger = $params['deprecationLogger'] ?? static function ( $msg ) { |
| 44 | trigger_error( $msg, E_USER_DEPRECATED ); |
| 45 | }; |
| 46 | $this->csProvider = $params['criticalSectionProvider'] ?? null; |
| 47 | $this->profiler = $params['profiler'] ?? null; |
| 48 | $this->tracer = $params['tracer'] ?? new NoopTracer(); |
| 49 | $this->cliMode = $params['cliMode'] ?? ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' ); |
| 50 | $this->debugSql = $params['debugSql'] ?? false; |
| 51 | } |
| 52 | |
| 53 | /** |
| 54 | * Construct a Database subclass instance given a database type and parameters |
| 55 | * |
| 56 | * This also connects to the database immediately upon object construction |
| 57 | * |
| 58 | * @param string $type A possible DB type (sqlite, mysql, postgres,...) |
| 59 | * @param array $params Parameter map with keys: |
| 60 | * - host : The hostname of the DB server |
| 61 | * - user : The name of the database user the client operates under |
| 62 | * - password : The password for the database user |
| 63 | * - dbname : The name of the database to use where queries do not specify one. |
| 64 | * The database must exist or an error might be thrown. Setting this to the empty string |
| 65 | * will avoid any such errors and make the handle have no implicit database scope. This is |
| 66 | * useful for queries like SHOW STATUS, CREATE DATABASE, or DROP DATABASE. Note that a |
| 67 | * "database" in Postgres is rougly equivalent to an entire MySQL server. This the domain |
| 68 | * in which user names and such are defined, e.g. users are database-specific in Postgres. |
| 69 | * - schema : The database schema to use (if supported). A "schema" in Postgres is roughly |
| 70 | * equivalent to a "database" in MySQL. Note that MySQL and SQLite do not use schemas. |
| 71 | * - tablePrefix : Optional table prefix that is implicitly added on to all table names |
| 72 | * recognized in queries. This can be used in place of schemas for handle site farms. |
| 73 | * - flags : Optional bit field of DBO_* constants that define connection, protocol, |
| 74 | * buffering, and transaction behavior. It is STRONGLY adviced to leave the DBO_DEFAULT |
| 75 | * flag in place UNLESS this this database simply acts as a key/value store. |
| 76 | * - ssl : Whether to use TLS connections. |
| 77 | * - strictWarnings: Whether to check for warnings and throw an exception if an unacceptable |
| 78 | * warning is found. |
| 79 | * - driver: Optional name of a specific DB client driver. For MySQL, there is only the |
| 80 | * 'mysqli' driver; the old one 'mysql' has been removed. |
| 81 | * - variables: Optional map of session variables to set after connecting. This can be |
| 82 | * used to adjust lock timeouts or encoding modes and the like. |
| 83 | * - topologyRole: Optional IDatabase::ROLE_* constant for the server. |
| 84 | * - lbInfo: Optional map of field/values for the managing load balancer instance. |
| 85 | * The "master" and "replica" fields are used to flag the replication role of this |
| 86 | * database server and whether methods like getLag() should actually issue queries. |
| 87 | * - connectTimeout: Optional timeout, in seconds, for connection attempts. |
| 88 | * - receiveTimeout: Optional timeout, in seconds, for receiving query results. |
| 89 | * - logger: Optional PSR-3 logger interface instance. |
| 90 | * - tracer: Optional TracerInterface instance. |
| 91 | * - profiler : Optional callback that takes a section name argument and returns |
| 92 | * a ScopedCallback instance that ends the profile section in its destructor. |
| 93 | * These will be called in query(), using a simplified version of the SQL that |
| 94 | * also includes the agent as a SQL comment. |
| 95 | * - trxProfiler: Optional TransactionProfiler instance. |
| 96 | * - errorLogger: Optional callback that takes an Exception and logs it. |
| 97 | * - deprecationLogger: Optional callback that takes a string and logs it. |
| 98 | * - cliMode: Whether to consider the execution context that of a CLI script. |
| 99 | * - agent: Optional name used to identify the end-user in query profiling/logging. |
| 100 | * - serverName: Optional human-readable server name |
| 101 | * - srvCache: Optional BagOStuff instance to an APC-style cache. |
| 102 | * - nonNativeInsertSelectBatchSize: Optional batch size for non-native INSERT SELECT. |
| 103 | * @param int $connect One of the class constants (NEW_CONNECTED, NEW_UNCONNECTED) [optional] |
| 104 | * @return Database|null If the database driver or extension cannot be found |
| 105 | * @throws InvalidArgumentException If the database driver or extension cannot be found |
| 106 | */ |
| 107 | public function create( $type, $params = [], $connect = Database::NEW_CONNECTED ) { |
| 108 | $class = $this->getClass( $type, $params['driver'] ?? null ); |
| 109 | |
| 110 | if ( class_exists( $class ) && is_subclass_of( $class, IDatabase::class ) ) { |
| 111 | $params += [ |
| 112 | // Default configuration |
| 113 | 'host' => null, |
| 114 | 'user' => null, |
| 115 | 'password' => null, |
| 116 | 'dbname' => null, |
| 117 | 'schema' => null, |
| 118 | 'tablePrefix' => '', |
| 119 | 'variables' => [], |
| 120 | 'lbInfo' => [], |
| 121 | 'serverName' => null, |
| 122 | 'topologyRole' => null, |
| 123 | // Objects and callbacks |
| 124 | 'srvCache' => $params['srvCache'] ?? new HashBagOStuff(), |
| 125 | 'trxProfiler' => $params['trxProfiler'] ?? new TransactionProfiler(), |
| 126 | 'logger' => $params['logger'] ?? new NullLogger(), |
| 127 | 'errorLogger' => $params['errorLogger'] ?? static function ( Throwable $e ) { |
| 128 | trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_USER_WARNING ); |
| 129 | }, |
| 130 | ]; |
| 131 | |
| 132 | $params['flags'] ??= 0; |
| 133 | if ( $this->debugSql ) { |
| 134 | $params['flags'] |= DBO_DEBUG; |
| 135 | } |
| 136 | |
| 137 | $overrides = [ |
| 138 | 'flags' => $this->initConnectionFlags( $params['flags'] ), |
| 139 | 'cliMode' => $this->cliMode, |
| 140 | 'agent' => $this->agent, |
| 141 | 'profiler' => $this->profiler, |
| 142 | 'deprecationLogger' => $this->deprecationLogger, |
| 143 | 'criticalSectionProvider' => $this->csProvider, |
| 144 | 'tracer' => $this->tracer, |
| 145 | ]; |
| 146 | |
| 147 | /** @var Database $conn */ |
| 148 | $conn = new $class( array_merge( $params, $overrides ) ); |
| 149 | if ( $connect === Database::NEW_CONNECTED ) { |
| 150 | $conn->initConnection(); |
| 151 | } |
| 152 | } else { |
| 153 | $conn = null; |
| 154 | } |
| 155 | |
| 156 | return $conn; |
| 157 | } |
| 158 | |
| 159 | /** |
| 160 | * @param string $dbType A possible DB type (sqlite, mysql, postgres,...) |
| 161 | * @param string|null $driver Optional name of a specific DB client driver |
| 162 | * @return array Map of (Database::ATTR_* constant => value) for all such constants |
| 163 | * @throws DBUnexpectedError |
| 164 | */ |
| 165 | public function attributesFromType( $dbType, $driver = null ) { |
| 166 | static $defaults = [ |
| 167 | Database::ATTR_DB_IS_FILE => false, |
| 168 | Database::ATTR_DB_LEVEL_LOCKING => false, |
| 169 | Database::ATTR_SCHEMAS_AS_TABLE_GROUPS => false |
| 170 | ]; |
| 171 | |
| 172 | $class = $this->getClass( $dbType, $driver ); |
| 173 | if ( class_exists( $class ) ) { |
| 174 | return $class::getAttributes() + $defaults; |
| 175 | } else { |
| 176 | throw new DBUnexpectedError( null, "$dbType is not a supported database type." ); |
| 177 | } |
| 178 | } |
| 179 | |
| 180 | /** |
| 181 | * @param string $dbType A possible DB type (sqlite, mysql, postgres,...) |
| 182 | * @param string|null $driver Optional name of a specific DB client driver |
| 183 | * @return class-string<Database> Database subclass name to use |
| 184 | * @throws InvalidArgumentException |
| 185 | */ |
| 186 | protected function getClass( $dbType, $driver = null ) { |
| 187 | // For database types with built-in support, the below maps type to IDatabase |
| 188 | // implementations. For types with multiple driver implementations (PHP extensions), |
| 189 | // an array can be used, keyed by extension name. In case of an array, the |
| 190 | // optional 'driver' parameter can be used to force a specific driver. Otherwise, |
| 191 | // we auto-detect the first available driver. For types without built-in support, |
| 192 | // a class named "Database<Type>" is used, eg. DatabaseFoo for type 'foo'. |
| 193 | static $builtinTypes = [ |
| 194 | 'mysql' => [ 'mysqli' => DatabaseMySQL::class ], |
| 195 | 'sqlite' => DatabaseSqlite::class, |
| 196 | 'postgres' => DatabasePostgres::class, |
| 197 | ]; |
| 198 | |
| 199 | $dbType = strtolower( $dbType ); |
| 200 | |
| 201 | if ( !isset( $builtinTypes[$dbType] ) ) { |
| 202 | // Not a built in type, assume standard naming scheme |
| 203 | return 'Database' . ucfirst( $dbType ); |
| 204 | } |
| 205 | |
| 206 | $class = false; |
| 207 | $possibleDrivers = $builtinTypes[$dbType]; |
| 208 | if ( is_string( $possibleDrivers ) ) { |
| 209 | $class = $possibleDrivers; |
| 210 | } elseif ( (string)$driver !== '' ) { |
| 211 | if ( !isset( $possibleDrivers[$driver] ) ) { |
| 212 | throw new InvalidArgumentException( __METHOD__ . |
| 213 | " type '$dbType' does not support driver '{$driver}'" ); |
| 214 | } |
| 215 | |
| 216 | $class = $possibleDrivers[$driver]; |
| 217 | } else { |
| 218 | foreach ( $possibleDrivers as $posDriver => $possibleClass ) { |
| 219 | if ( extension_loaded( $posDriver ) ) { |
| 220 | $class = $possibleClass; |
| 221 | break; |
| 222 | } |
| 223 | } |
| 224 | } |
| 225 | |
| 226 | if ( $class === false ) { |
| 227 | throw new InvalidArgumentException( __METHOD__ . |
| 228 | " no viable database extension found for type '$dbType'" ); |
| 229 | } |
| 230 | |
| 231 | return $class; |
| 232 | } |
| 233 | |
| 234 | /** |
| 235 | * @see IDatabase::DBO_DEFAULT |
| 236 | * @param int $flags Bit field of IDatabase::DBO_* constants from configuration |
| 237 | * @return int Bit field of IDatabase::DBO_* constants to use with Database::factory() |
| 238 | */ |
| 239 | private function initConnectionFlags( int $flags ) { |
| 240 | if ( self::fieldHasBit( $flags, IDatabase::DBO_DEFAULT ) ) { |
| 241 | // Server is configured to participate in transaction rounds in non-CLI mode |
| 242 | if ( $this->cliMode ) { |
| 243 | $flags &= ~IDatabase::DBO_TRX; |
| 244 | } else { |
| 245 | $flags |= IDatabase::DBO_TRX; |
| 246 | } |
| 247 | } |
| 248 | return $flags; |
| 249 | } |
| 250 | |
| 251 | /** |
| 252 | * @param int $flags A bitfield of flags |
| 253 | * @param int $bit Bit flag constant |
| 254 | * @return bool Whether the bit field has the specified bit flag set |
| 255 | */ |
| 256 | private function fieldHasBit( int $flags, int $bit ) { |
| 257 | return ( ( $flags & $bit ) === $bit ); |
| 258 | } |
| 259 | } |