Completed
Push — 4 ( 6a8c67...aeefb9 )
by Guy
40:23 queued 33:07
created

Database::getWhitelistQueryArray()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
1
<?php
2
3
namespace SilverStripe\ORM\Connect;
4
5
use SilverStripe\Control\Director;
6
use SilverStripe\Core\Config\Config;
7
use SilverStripe\Dev\Debug;
8
use SilverStripe\ORM\DB;
9
use SilverStripe\ORM\PaginatedList;
10
use SilverStripe\ORM\Queries\SQLUpdate;
11
use SilverStripe\ORM\Queries\SQLInsert;
12
use BadMethodCallException;
13
use Exception;
14
use SilverStripe\Dev\Backtrace;
15
16
/**
17
 * Abstract database connectivity class.
18
 * Sub-classes of this implement the actual database connection libraries
19
 */
20
abstract class Database
21
{
22
23
    const PARTIAL_QUERY = 'partial_query';
24
    const FULL_QUERY = 'full_query';
25
26
    /**
27
     * To use, call from _config.php
28
     * Example:
29
     * <code>
30
     * Database::setWhitelistQueryArray([
31
     *      'Qualmark' => 'partial_query',
32
     *      'SELECT "Version" FROM "SiteTree_Live" WHERE "ID" = ?' => 'full_query',
33
     * ])
34
     * </code>
35
     * @var array
36
     */
37
    protected static $whitelist_array = [];
38
39
    /**
40
     * Database connector object
41
     *
42
     * @var DBConnector
43
     */
44
    protected $connector = null;
45
46
    /**
47
     * In cases where your environment does not have 'SHOW DATABASES' permission,
48
     * you can set this to true. Then selectDatabase() will always connect without
49
     * doing databaseExists() check.
50
     *
51
     * @var bool
52
     */
53
    private static $optimistic_connect = false;
0 ignored issues
show
introduced by
The private property $optimistic_connect is not used, and could be removed.
Loading history...
54
55
    /**
56
     * Amount of queries executed, for debugging purposes.
57
     *
58
     * @var int
59
     */
60
    protected $queryCount = 0;
61
62
    /**
63
     * Get the current connector
64
     *
65
     * @return DBConnector
66
     */
67
    public function getConnector()
68
    {
69
        return $this->connector;
70
    }
71
72
    /**
73
     * Injector injection point for connector dependency
74
     *
75
     * @param DBConnector $connector
76
     */
77
    public function setConnector(DBConnector $connector)
78
    {
79
        $this->connector = $connector;
80
    }
81
82
    /**
83
     * Database schema manager object
84
     *
85
     * @var DBSchemaManager
86
     */
87
    protected $schemaManager = null;
88
89
    /**
90
     * Returns the current schema manager
91
     *
92
     * @return DBSchemaManager
93
     */
94
    public function getSchemaManager()
95
    {
96
        return $this->schemaManager;
97
    }
98
99
    /**
100
     * Injector injection point for schema manager
101
     *
102
     * @param DBSchemaManager $schemaManager
103
     */
104
    public function setSchemaManager(DBSchemaManager $schemaManager)
105
    {
106
        $this->schemaManager = $schemaManager;
107
108
        if ($this->schemaManager) {
109
            $this->schemaManager->setDatabase($this);
110
        }
111
    }
112
113
    /**
114
     * Query builder object
115
     *
116
     * @var DBQueryBuilder
117
     */
118
    protected $queryBuilder = null;
119
120
    /**
121
     * Returns the current query builder
122
     *
123
     * @return DBQueryBuilder
124
     */
125
    public function getQueryBuilder()
126
    {
127
        return $this->queryBuilder;
128
    }
129
130
    /**
131
     * Injector injection point for schema manager
132
     *
133
     * @param DBQueryBuilder $queryBuilder
134
     */
135
    public function setQueryBuilder(DBQueryBuilder $queryBuilder)
136
    {
137
        $this->queryBuilder = $queryBuilder;
138
    }
139
140
    /**
141
     * Execute the given SQL query.
142
     *
143
     * @param string $sql The SQL query to execute
144
     * @param int $errorLevel The level of error reporting to enable for the query
145
     * @return Query
146
     */
147
    public function query($sql, $errorLevel = E_USER_ERROR)
148
    {
149
        // Check if we should only preview this query
150
        if ($this->previewWrite($sql)) {
151
            return null;
152
        }
153
154
        // Benchmark query
155
        $connector = $this->connector;
156
        return $this->benchmarkQuery(
157
            $sql,
158
            function ($sql) use ($connector, $errorLevel) {
159
                return $connector->query($sql, $errorLevel);
160
            }
161
        );
162
    }
163
164
165
    /**
166
     * Execute the given SQL parameterised query with the specified arguments
167
     *
168
     * @param string $sql The SQL query to execute. The ? character will denote parameters.
169
     * @param array $parameters An ordered list of arguments.
170
     * @param int $errorLevel The level of error reporting to enable for the query
171
     * @return Query
172
     */
173
    public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR)
174
    {
175
        // Check if we should only preview this query
176
        if ($this->previewWrite($sql)) {
177
            return null;
178
        }
179
180
        // Benchmark query
181
        $connector = $this->connector;
182
        return $this->benchmarkQuery(
183
            $sql,
184
            function ($sql) use ($connector, $parameters, $errorLevel) {
185
                return $connector->preparedQuery($sql, $parameters, $errorLevel);
186
            },
187
            $parameters
188
        );
189
    }
190
191
    /**
192
     * Determines if the query should be previewed, and thus interrupted silently.
193
     * If so, this function also displays the query via the debuging system.
194
     * Subclasess should respect the results of this call for each query, and not
195
     * execute any queries that generate a true response.
196
     *
197
     * @param string $sql The query to be executed
198
     * @return boolean Flag indicating that the query was previewed
199
     */
200
    protected function previewWrite($sql)
201
    {
202
        // Only preview if previewWrite is set, we are in dev mode, and
203
        // the query is mutable
204
        if (isset($_REQUEST['previewwrite'])
205
            && Director::isDev()
206
            && $this->connector->isQueryMutable($sql)
207
        ) {
208
            // output preview message
209
            Debug::message("Will execute: $sql");
210
            return true;
211
        } else {
212
            return false;
213
        }
214
    }
215
216
    /**
217
     * Allows the display and benchmarking of queries as they are being run
218
     *
219
     * @param string $sql Query to run, and single parameter to callback
220
     * @param callable $callback Callback to execute code
221
     * @param array $parameters Parameters for any parameterised query
222
     * @return mixed Result of query
223
     */
224
    protected function benchmarkQuery($sql, $callback, $parameters = array())
225
    {
226
        if (isset($_REQUEST['showqueries']) && Director::isDev()) {
227
            $displaySql = true;
228
            $this->queryCount++;
229
            $starttime = microtime(true);
230
            $result = $callback($sql);
231
            $endtime = round(microtime(true) - $starttime, 4);
232
            // replace parameters as closely as possible to what we'd expect the DB to put in
233
            if (in_array(strtolower($_REQUEST['showqueries']), ['inline', 'backtrace'])) {
234
                $sql = DB::inline_parameters($sql, $parameters);
235
            } elseif (strtolower($_REQUEST['showqueries']) === 'whitelist') {
236
                $displaySql = false;
237
                foreach (self::$whitelist_array as $query => $searchType) {
238
                    $fullQuery = ($searchType === self::FULL_QUERY && $query === $sql);
239
                    $partialQuery = ($searchType === self::PARTIAL_QUERY && mb_strpos($sql, $query) !== false);
240
                    if (!$fullQuery && !$partialQuery) {
241
                        continue;
242
                    }
243
                    $sql = DB::inline_parameters($sql, $parameters);
244
                    $this->displayQuery($sql, $endtime);
245
                }
246
            }
247
248
            if ($displaySql) {
249
                $this->displayQuery($sql, $endtime);
250
            }
251
252
            // Show a backtrace if ?showqueries=backtrace
253
            if ($_REQUEST['showqueries'] === 'backtrace') {
254
                Backtrace::backtrace();
255
            }
256
            return $result;
257
        } else {
258
            return $callback($sql);
259
        }
260
    }
261
262
    /**
263
     * Display query message
264
     *
265
     * @param mixed $query
266
     * @param float $endtime
267
     */
268
    protected function displayQuery($query, $endtime)
269
    {
270
        $queryCount = sprintf("%04d", $this->queryCount);
271
        Debug::message("\n$queryCount: $query\n{$endtime}s\n", false);
272
    }
273
274
    /**
275
     * Add the sql queries that need to be partially or fully matched
276
     *
277
     * @param array $whitelistArray
278
     */
279
    public static function setWhitelistQueryArray($whitelistArray)
280
    {
281
        self::$whitelist_array = $whitelistArray;
282
    }
283
284
    /**
285
     * Get the sql queries that need to be partially or fully matched
286
     *
287
     * @return array
288
     */
289
    public static function getWhitelistQueryArray()
290
    {
291
        return self::$whitelist_array;
292
    }
293
294
    /**
295
     * Get the autogenerated ID from the previous INSERT query.
296
     *
297
     * @param string $table The name of the table to get the generated ID for
298
     * @return integer the most recently generated ID for the specified table
299
     */
300
    public function getGeneratedID($table)
301
    {
302
        return $this->connector->getGeneratedID($table);
303
    }
304
305
    /**
306
     * Determines if we are connected to a server AND have a valid database
307
     * selected.
308
     *
309
     * @return boolean Flag indicating that a valid database is connected
310
     */
311
    public function isActive()
312
    {
313
        return $this->connector->isActive();
314
    }
315
316
    /**
317
     * Returns an escaped string. This string won't be quoted, so would be suitable
318
     * for appending to other quoted strings.
319
     *
320
     * @param mixed $value Value to be prepared for database query
321
     * @return string Prepared string
322
     */
323
    public function escapeString($value)
324
    {
325
        return $this->connector->escapeString($value);
326
    }
327
328
    /**
329
     * Wrap a string into DB-specific quotes.
330
     *
331
     * @param mixed $value Value to be prepared for database query
332
     * @return string Prepared string
333
     */
334
    public function quoteString($value)
335
    {
336
        return $this->connector->quoteString($value);
337
    }
338
339
    /**
340
     * Escapes an identifier (table / database name). Typically the value
341
     * is simply double quoted. Don't pass in already escaped identifiers in,
342
     * as this will double escape the value!
343
     *
344
     * @param string|array $value The identifier to escape or list of split components
345
     * @param string $separator Splitter for each component
346
     * @return string
347
     */
348
    public function escapeIdentifier($value, $separator = '.')
349
    {
350
        // Split string into components
351
        if (!is_array($value)) {
352
            $value = explode($separator, $value);
353
        }
354
355
        // Implode quoted column
356
        return '"' . implode('"' . $separator . '"', $value) . '"';
357
    }
358
359
    /**
360
     * Escapes unquoted columns keys in an associative array
361
     *
362
     * @param array $fieldValues
363
     * @return array List of field values with the keys as escaped column names
364
     */
365
    protected function escapeColumnKeys($fieldValues)
366
    {
367
        $out = array();
368
        foreach ($fieldValues as $field => $value) {
369
            $out[$this->escapeIdentifier($field)] = $value;
370
        }
371
        return $out;
372
    }
373
374
    /**
375
     * Execute a complex manipulation on the database.
376
     * A manipulation is an array of insert / or update sequences.  The keys of the array are table names,
377
     * and the values are map containing 'command' and 'fields'.  Command should be 'insert' or 'update',
378
     * and fields should be a map of field names to field values, NOT including quotes.
379
     *
380
     * The field values could also be in paramaterised format, such as
381
     * array('MAX(?,?)' => array(42, 69)), allowing the use of raw SQL values such as
382
     * array('NOW()' => array()).
383
     *
384
     * @see SQLWriteExpression::addAssignments for syntax examples
385
     *
386
     * @param array $manipulation
387
     */
388
    public function manipulate($manipulation)
389
    {
390
        if (empty($manipulation)) {
391
            return;
392
        }
393
394
        foreach ($manipulation as $table => $writeInfo) {
395
            if (empty($writeInfo['fields'])) {
396
                continue;
397
            }
398
            // Note: keys of $fieldValues are not escaped
399
            $fieldValues = $writeInfo['fields'];
400
401
            // Switch command type
402
            switch ($writeInfo['command']) {
403
                case "update":
404
                    // Build update
405
                    $query = new SQLUpdate("\"$table\"", $this->escapeColumnKeys($fieldValues));
406
407
                    // Set best condition to use
408
                    if (!empty($writeInfo['where'])) {
409
                        $query->addWhere($writeInfo['where']);
410
                    } elseif (!empty($writeInfo['id'])) {
411
                        $query->addWhere(array('"ID"' => $writeInfo['id']));
412
                    }
413
414
                    // Test to see if this update query shouldn't, in fact, be an insert
415
                    if ($query->toSelect()->count()) {
416
                        $query->execute();
417
                        break;
418
                    }
419
                    // ...if not, we'll skip on to the insert code
420
421
                case "insert":
422
                    // Ensure that the ID clause is given if possible
423
                    if (!isset($fieldValues['ID']) && isset($writeInfo['id'])) {
424
                        $fieldValues['ID'] = $writeInfo['id'];
425
                    }
426
427
                    // Build insert
428
                    $query = new SQLInsert("\"$table\"", $this->escapeColumnKeys($fieldValues));
429
430
                    $query->execute();
431
                    break;
432
433
                default:
434
                    user_error(
435
                        "SS_Database::manipulate() Can't recognise command '{$writeInfo['command']}'",
436
                        E_USER_ERROR
437
                    );
438
            }
439
        }
440
    }
441
442
    /**
443
     * Enable supression of database messages.
444
     */
445
    public function quiet()
446
    {
447
        $this->schemaManager->quiet();
448
    }
449
450
    /**
451
     * Clear all data out of the database
452
     */
453
    public function clearAllData()
454
    {
455
        $tables = $this->getSchemaManager()->tableList();
456
        foreach ($tables as $table) {
457
            $this->clearTable($table);
458
        }
459
    }
460
461
    /**
462
     * Clear all data in a given table
463
     *
464
     * @param string $table Name of table
465
     */
466
    public function clearTable($table)
467
    {
468
        $this->query("TRUNCATE \"$table\"");
469
    }
470
471
    /**
472
     * Generates a WHERE clause for null comparison check
473
     *
474
     * @param string $field Quoted field name
475
     * @param bool $isNull Whether to check for NULL or NOT NULL
476
     * @return string Non-parameterised null comparison clause
477
     */
478
    public function nullCheckClause($field, $isNull)
479
    {
480
        $clause = $isNull
481
            ? "%s IS NULL"
482
            : "%s IS NOT NULL";
483
        return sprintf($clause, $field);
484
    }
485
486
    /**
487
     * Generate a WHERE clause for text matching.
488
     *
489
     * @param String $field Quoted field name
490
     * @param String $value Escaped search. Can include percentage wildcards.
491
     * Ignored if $parameterised is true.
492
     * @param boolean $exact Exact matches or wildcard support.
493
     * @param boolean $negate Negate the clause.
494
     * @param boolean $caseSensitive Enforce case sensitivity if TRUE or FALSE.
495
     * Fallback to default collation if set to NULL.
496
     * @param boolean $parameterised Insert the ? placeholder rather than the
497
     * given value. If this is true then $value is ignored.
498
     * @return String SQL
499
     */
500
    abstract public function comparisonClause(
501
        $field,
502
        $value,
503
        $exact = false,
504
        $negate = false,
505
        $caseSensitive = null,
506
        $parameterised = false
507
    );
508
509
    /**
510
     * function to return an SQL datetime expression that can be used with the adapter in use
511
     * used for querying a datetime in a certain format
512
     *
513
     * @param string $date to be formated, can be either 'now', literal datetime like '1973-10-14 10:30:00' or
514
     *                     field name, e.g. '"SiteTree"."Created"'
515
     * @param string $format to be used, supported specifiers:
516
     * %Y = Year (four digits)
517
     * %m = Month (01..12)
518
     * %d = Day (01..31)
519
     * %H = Hour (00..23)
520
     * %i = Minutes (00..59)
521
     * %s = Seconds (00..59)
522
     * %U = unix timestamp, can only be used on it's own
523
     * @return string SQL datetime expression to query for a formatted datetime
524
     */
525
    abstract public function formattedDatetimeClause($date, $format);
526
527
    /**
528
     * function to return an SQL datetime expression that can be used with the adapter in use
529
     * used for querying a datetime addition
530
     *
531
     * @param string $date, can be either 'now', literal datetime like '1973-10-14 10:30:00' or field name,
532
     *                      e.g. '"SiteTree"."Created"'
533
     * @param string $interval to be added, use the format [sign][integer] [qualifier], e.g. -1 Day, +15 minutes,
534
     *                         +1 YEAR
535
     * supported qualifiers:
536
     * - years
537
     * - months
538
     * - days
539
     * - hours
540
     * - minutes
541
     * - seconds
542
     * This includes the singular forms as well
543
     * @return string SQL datetime expression to query for a datetime (YYYY-MM-DD hh:mm:ss) which is the result of
544
     *                the addition
545
     */
546
    abstract public function datetimeIntervalClause($date, $interval);
547
548
    /**
549
     * function to return an SQL datetime expression that can be used with the adapter in use
550
     * used for querying a datetime substraction
551
     *
552
     * @param string $date1, can be either 'now', literal datetime like '1973-10-14 10:30:00' or field name
553
     *                       e.g. '"SiteTree"."Created"'
554
     * @param string $date2 to be substracted of $date1, can be either 'now', literal datetime
555
     *                      like '1973-10-14 10:30:00' or field name, e.g. '"SiteTree"."Created"'
556
     * @return string SQL datetime expression to query for the interval between $date1 and $date2 in seconds which
557
     *                is the result of the substraction
558
     */
559
    abstract public function datetimeDifferenceClause($date1, $date2);
560
561
    /**
562
     * String operator for concatenation of strings
563
     *
564
     * @return string
565
     */
566
    public function concatOperator()
567
    {
568
        // @todo Make ' + ' in mssql
569
        return ' || ';
570
    }
571
572
    /**
573
     * Returns true if this database supports collations
574
     *
575
     * @return boolean
576
     */
577
    abstract public function supportsCollations();
578
579
    /**
580
     * Can the database override timezone as a connection setting,
581
     * or does it use the system timezone exclusively?
582
     *
583
     * @return Boolean
584
     */
585
    abstract public function supportsTimezoneOverride();
586
587
    /**
588
     * Query for the version of the currently connected database
589
     * @return string Version of this database
590
     */
591
    public function getVersion()
592
    {
593
        return $this->connector->getVersion();
594
    }
595
596
    /**
597
     * Get the database server type (e.g. mysql, postgresql).
598
     * This value is passed to the connector as the 'driver' argument when
599
     * initiating a database connection
600
     *
601
     * @return string
602
     */
603
    abstract public function getDatabaseServer();
604
605
    /**
606
     * Return the number of rows affected by the previous operation.
607
     * @return int
608
     */
609
    public function affectedRows()
610
    {
611
        return $this->connector->affectedRows();
612
    }
613
614
    /**
615
     * The core search engine, used by this class and its subclasses to do fun stuff.
616
     * Searches both SiteTree and File.
617
     *
618
     * @param array $classesToSearch List of classes to search
619
     * @param string $keywords Keywords as a string.
620
     * @param integer $start Item to start returning results from
621
     * @param integer $pageLength Number of items per page
622
     * @param string $sortBy Sort order expression
623
     * @param string $extraFilter Additional filter
624
     * @param boolean $booleanSearch Flag for boolean search mode
625
     * @param string $alternativeFileFilter
626
     * @param boolean $invertedMatch
627
     * @return PaginatedList Search results
628
     */
629
    abstract public function searchEngine(
630
        $classesToSearch,
631
        $keywords,
632
        $start,
633
        $pageLength,
634
        $sortBy = "Relevance DESC",
635
        $extraFilter = "",
636
        $booleanSearch = false,
637
        $alternativeFileFilter = "",
638
        $invertedMatch = false
639
    );
640
641
    /**
642
     * Determines if this database supports transactions
643
     *
644
     * @return boolean Flag indicating support for transactions
645
     */
646
    abstract public function supportsTransactions();
647
648
    /**
649
     * Does this database support savepoints in transactions
650
     * By default it is assumed that they don't unless they are explicitly enabled.
651
     *
652
     * @return boolean Flag indicating support for savepoints in transactions
653
     */
654
    public function supportsSavepoints()
655
    {
656
        return false;
657
    }
658
659
    /**
660
     * Invoke $callback within a transaction
661
     *
662
     * @param callable $callback Callback to run
663
     * @param callable $errorCallback Optional callback to run after rolling back transaction.
664
     * @param bool|string $transactionMode Optional transaction mode to use
665
     * @param bool $errorIfTransactionsUnsupported If true, this method will fail if transactions are unsupported.
666
     * Otherwise, the $callback will potentially be invoked outside of a transaction.
667
     * @throws Exception
668
     */
669
    public function withTransaction(
670
        $callback,
671
        $errorCallback = null,
672
        $transactionMode = false,
673
        $errorIfTransactionsUnsupported = false
674
    ) {
675
        $supported = $this->supportsTransactions();
676
        if (!$supported && $errorIfTransactionsUnsupported) {
677
            throw new BadMethodCallException("Transactions not supported by this database.");
678
        }
679
        if ($supported) {
680
            $this->transactionStart($transactionMode);
681
        }
682
        try {
683
            call_user_func($callback);
684
        } catch (Exception $ex) {
685
            if ($supported) {
686
                $this->transactionRollback();
687
            }
688
            if ($errorCallback) {
689
                call_user_func($errorCallback);
690
            }
691
            throw $ex;
692
        }
693
        if ($supported) {
694
            $this->transactionEnd();
695
        }
696
    }
697
698
    /*
699
     * Determines if the current database connection supports a given list of extensions
700
     *
701
     * @param array $extensions List of extensions to check for support of. The key of this array
702
     * will be an extension name, and the value the configuration for that extension. This
703
     * could be one of partitions, tablespaces, or clustering
704
     * @return boolean Flag indicating support for all of the above
705
     * @todo Write test cases
706
     */
707
    public function supportsExtensions($extensions)
0 ignored issues
show
Unused Code introduced by
The parameter $extensions is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

707
    public function supportsExtensions(/** @scrutinizer ignore-unused */ $extensions)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
708
    {
709
        return false;
710
    }
711
712
    /**
713
     * Start a prepared transaction
714
     * See http://developer.postgresql.org/pgdocs/postgres/sql-set-transaction.html for details on
715
     * transaction isolation options
716
     *
717
     * @param string|boolean $transactionMode Transaction mode, or false to ignore
718
     * @param string|boolean $sessionCharacteristics Session characteristics, or false to ignore
719
     */
720
    abstract public function transactionStart($transactionMode = false, $sessionCharacteristics = false);
721
722
    /**
723
     * Create a savepoint that you can jump back to if you encounter problems
724
     *
725
     * @param string $savepoint Name of savepoint
726
     */
727
    abstract public function transactionSavepoint($savepoint);
728
729
    /**
730
     * Rollback or revert to a savepoint if your queries encounter problems
731
     * If you encounter a problem at any point during a transaction, you may
732
     * need to rollback that particular query, or return to a savepoint
733
     *
734
     * @param string|boolean $savepoint Name of savepoint, or leave empty to rollback
735
     * to last savepoint
736
     * @return bool|null Boolean is returned if success state is known, or null if
737
     * unknown. Note: For error checking purposes null should not be treated as error.
738
     */
739
    abstract public function transactionRollback($savepoint = false);
740
741
    /**
742
     * Commit everything inside this transaction so far
743
     *
744
     * @param bool $chain
745
     * @return bool|null Boolean is returned if success state is known, or null if
746
     * unknown. Note: For error checking purposes null should not be treated as error.
747
     */
748
    abstract public function transactionEnd($chain = false);
749
750
    /**
751
     * Return depth of current transaction
752
     *
753
     * @return int Nesting level, or 0 if not in a transaction
754
     */
755
    public function transactionDepth()
756
    {
757
        // Placeholder error for transactional DBs that don't expose depth
758
        if ($this->supportsTransactions()) {
759
            user_error(get_class($this) . " does not support transactionDepth", E_USER_WARNING);
760
        }
761
        return 0;
762
    }
763
764
    /**
765
     * Determines if the used database supports application-level locks,
766
     * which is different from table- or row-level locking.
767
     * See {@link getLock()} for details.
768
     *
769
     * @return bool Flag indicating that locking is available
770
     */
771
    public function supportsLocks()
772
    {
773
        return false;
774
    }
775
776
    /**
777
     * Returns if the lock is available.
778
     * See {@link supportsLocks()} to check if locking is generally supported.
779
     *
780
     * @param string $name Name of the lock
781
     * @return bool
782
     */
783
    public function canLock($name)
784
    {
785
        return false;
786
    }
787
788
    /**
789
     * Sets an application-level lock so that no two processes can run at the same time,
790
     * also called a "cooperative advisory lock".
791
     *
792
     * Return FALSE if acquiring the lock fails; otherwise return TRUE, if lock was acquired successfully.
793
     * Lock is automatically released if connection to the database is broken (either normally or abnormally),
794
     * making it less prone to deadlocks than session- or file-based locks.
795
     * Should be accompanied by a {@link releaseLock()} call after the logic requiring the lock has completed.
796
     * Can be called multiple times, in which case locks "stack" (PostgreSQL, SQL Server),
797
     * or auto-releases the previous lock (MySQL).
798
     *
799
     * Note that this might trigger the database to wait for the lock to be released, delaying further execution.
800
     *
801
     * @param string $name Name of lock
802
     * @param integer $timeout Timeout in seconds
803
     * @return bool
804
     */
805
    public function getLock($name, $timeout = 5)
0 ignored issues
show
Unused Code introduced by
The parameter $timeout is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

805
    public function getLock($name, /** @scrutinizer ignore-unused */ $timeout = 5)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
806
    {
807
        return false;
808
    }
809
810
    /**
811
     * Remove an application-level lock file to allow another process to run
812
     * (if the execution aborts (e.g. due to an error) all locks are automatically released).
813
     *
814
     * @param string $name Name of the lock
815
     * @return bool Flag indicating whether the lock was successfully released
816
     */
817
    public function releaseLock($name)
818
    {
819
        return false;
820
    }
821
822
    /**
823
     * Instruct the database to generate a live connection
824
     *
825
     * @param array $parameters An map of parameters, which should include:
826
     *  - server: The server, eg, localhost
827
     *  - username: The username to log on with
828
     *  - password: The password to log on with
829
     *  - database: The database to connect to
830
     *  - charset: The character set to use. Defaults to utf8
831
     *  - timezone: (optional) The timezone offset. For example: +12:00, "Pacific/Auckland", or "SYSTEM"
832
     *  - driver: (optional) Driver name
833
     */
834
    public function connect($parameters)
835
    {
836
        // Ensure that driver is available (required by PDO)
837
        if (empty($parameters['driver'])) {
838
            $parameters['driver'] = $this->getDatabaseServer();
839
        }
840
841
        // Notify connector of parameters
842
        $this->connector->connect($parameters);
843
844
        // SS_Database subclass maintains responsibility for selecting database
845
        // once connected in order to correctly handle schema queries about
846
        // existence of database, error handling at the correct level, etc
847
        if (!empty($parameters['database'])) {
848
            $this->selectDatabase($parameters['database'], false, false);
849
        }
850
    }
851
852
    /**
853
     * Determine if the database with the specified name exists
854
     *
855
     * @param string $name Name of the database to check for
856
     * @return bool Flag indicating whether this database exists
857
     */
858
    public function databaseExists($name)
859
    {
860
        return $this->schemaManager->databaseExists($name);
861
    }
862
863
    /**
864
     * Retrieves the list of all databases the user has access to
865
     *
866
     * @return array List of database names
867
     */
868
    public function databaseList()
869
    {
870
        return $this->schemaManager->databaseList();
871
    }
872
873
    /**
874
     * Change the connection to the specified database, optionally creating the
875
     * database if it doesn't exist in the current schema.
876
     *
877
     * @param string $name Name of the database
878
     * @param bool $create Flag indicating whether the database should be created
879
     * if it doesn't exist. If $create is false and the database doesn't exist
880
     * then an error will be raised
881
     * @param int|bool $errorLevel The level of error reporting to enable for the query, or false if no error
882
     * should be raised
883
     * @return bool Flag indicating success
884
     */
885
    public function selectDatabase($name, $create = false, $errorLevel = E_USER_ERROR)
886
    {
887
        // In case our live environment is locked down, we can bypass a SHOW DATABASE check
888
        $canConnect = Config::inst()->get(static::class, 'optimistic_connect')
889
            || $this->schemaManager->databaseExists($name);
890
        if ($canConnect) {
891
            return $this->connector->selectDatabase($name);
892
        }
893
894
        // Check DB creation permisson
895
        if (!$create) {
896
            if ($errorLevel !== false) {
897
                user_error("Attempted to connect to non-existing database \"$name\"", $errorLevel);
0 ignored issues
show
Bug introduced by
It seems like $errorLevel can also be of type true; however, parameter $error_type of user_error() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

897
                user_error("Attempted to connect to non-existing database \"$name\"", /** @scrutinizer ignore-type */ $errorLevel);
Loading history...
898
            }
899
            // Unselect database
900
            $this->connector->unloadDatabase();
901
            return false;
902
        }
903
        $this->schemaManager->createDatabase($name);
904
        return $this->connector->selectDatabase($name);
905
    }
906
907
    /**
908
     * Drop the database that this object is currently connected to.
909
     * Use with caution.
910
     */
911
    public function dropSelectedDatabase()
912
    {
913
        $databaseName = $this->connector->getSelectedDatabase();
914
        if ($databaseName) {
915
            $this->connector->unloadDatabase();
916
            $this->schemaManager->dropDatabase($databaseName);
917
        }
918
    }
919
920
    /**
921
     * Returns the name of the currently selected database
922
     *
923
     * @return string|null Name of the selected database, or null if none selected
924
     */
925
    public function getSelectedDatabase()
926
    {
927
        return $this->connector->getSelectedDatabase();
928
    }
929
930
    /**
931
     * Return SQL expression used to represent the current date/time
932
     *
933
     * @return string Expression for the current date/time
934
     */
935
    abstract public function now();
936
937
    /**
938
     * Returns the database-specific version of the random() function
939
     *
940
     * @return string Expression for a random value
941
     */
942
    abstract public function random();
943
}
944