MSSQLDatabase::query()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 5
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
1
<?php
2
3
namespace SilverStripe\MSSQL;
4
5
use SilverStripe\Core\Config\Configurable;
6
use SilverStripe\Core\Injector\Injectable;
7
use SilverStripe\Core\ClassInfo;
8
use SilverStripe\ORM\ArrayList;
9
use SilverStripe\ORM\Connect\Database;
10
use SilverStripe\ORM\DataList;
11
use SilverStripe\ORM\DB;
12
use SilverStripe\ORM\DataObject;
13
use SilverStripe\ORM\PaginatedList;
14
use SilverStripe\ORM\Queries\SQLSelect;
15
16
/**
17
 * Microsoft SQL Server 2008+ connector class.
18
 */
19
class MSSQLDatabase extends Database
20
{
21
    use Configurable;
22
    use Injectable;
23
24
    /**
25
     * Words that will trigger an error if passed to a SQL Server fulltext search
26
     */
27
    public static $noiseWords = array('about', '1', 'after', '2', 'all', 'also', '3', 'an', '4', 'and', '5', 'another', '6', 'any', '7', 'are', '8', 'as', '9', 'at', '0', 'be', '$', 'because', 'been', 'before', 'being', 'between', 'both', 'but', 'by', 'came', 'can', 'come', 'could', 'did', 'do', 'does', 'each', 'else', 'for', 'from', 'get', 'got', 'has', 'had', 'he', 'have', 'her', 'here', 'him', 'himself', 'his', 'how', 'if', 'in', 'into', 'is', 'it', 'its', 'just', 'like', 'make', 'many', 'me', 'might', 'more', 'most', 'much', 'must', 'my', 'never', 'no', 'now', 'of', 'on', 'only', 'or', 'other', 'our', 'out', 'over', 're', 'said', 'same', 'see', 'should', 'since', 'so', 'some', 'still', 'such', 'take', 'than', 'that', 'the', 'their', 'them', 'then', 'there', 'these', 'they', 'this', 'those', 'through', 'to', 'too', 'under', 'up', 'use', 'very', 'want', 'was', 'way', 'we', 'well', 'were', 'what', 'when', 'where', 'which', 'while', 'who', 'will', 'with', 'would', 'you', 'your', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z');
28
29
    /**
30
     * Transactions will work with FreeTDS, but not entirely with sqlsrv driver on Windows with MARS enabled.
31
     * TODO:
32
     * - after the test fails with open transaction, the transaction should be rolled back,
33
     *   otherwise other tests will break claiming that transaction is still open.
34
     * - figure out SAVEPOINTS
35
     * - READ ONLY transactions
36
     */
37
    protected $supportsTransactions = true;
38
39
    /**
40
     * Cached flag to determine if full-text is enabled. This is set by
41
     * {@link MSSQLDatabase::fullTextEnabled()}
42
     *
43
     * @var boolean
44
     */
45
    protected $fullTextEnabled = null;
46
47
    /**
48
     * @var bool
49
     */
50
    protected $transactionNesting = 0;
51
52
    /**
53
     * Set the default collation of the MSSQL nvarchar fields that we create.
54
     * We don't apply this to the database as a whole, so that we can use unicode collations.
55
     *
56
     * @param string $collation
57
     */
58
    public static function set_collation($collation)
59
    {
60
        static::config()->set('collation', $collation);
61
    }
62
63
    /**
64
     * The default collation of the MSSQL nvarchar fields that we create.
65
     * We don't apply this to the database as a whole, so that we can use
66
     * unicode collations.
67
     *
68
     * @return string
69
     */
70
    public static function get_collation()
71
    {
72
        return static::config()->get('collation');
73
    }
74
75
    /**
76
     * Connect to a MS SQL database.
77
     * @param array $parameters An map of parameters, which should include:
78
     *  - server: The server, eg, localhost
79
     *  - username: The username to log on with
80
     *  - password: The password to log on with
81
     *  - database: The database to connect to
82
     *  - windowsauthentication: Set to true to use windows authentication
83
     *    instead of username/password
84
     */
85
    public function connect($parameters)
86
    {
87
        parent::connect($parameters);
88
89
        // Configure the connection
90
        $this->query('SET QUOTED_IDENTIFIER ON');
91
        $this->query('SET TEXTSIZE 2147483647');
92
    }
93
94
    /**
95
     * Checks whether the current SQL Server version has full-text
96
     * support installed and full-text is enabled for this database.
97
     *
98
     * @return boolean
99
     */
100
    public function fullTextEnabled()
101
    {
102
        if ($this->fullTextEnabled === null) {
103
            $this->fullTextEnabled = $this->updateFullTextEnabled();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->updateFullTextEnabled() can also be of type string. However, the property $fullTextEnabled is declared as type boolean. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
104
        }
105
        return $this->fullTextEnabled;
106
    }
107
108
    /**
109
     * Checks whether the current SQL Server version has full-text
110
     * support installed and full-text is enabled for this database.
111
     *
112
     * @return boolean
113
     */
114
    protected function updateFullTextEnabled()
115
    {
116
        // Check if installed
117
        $isInstalled = $this->query("SELECT fulltextserviceproperty('isfulltextinstalled')")->value();
118
        if (!$isInstalled) {
119
            return false;
120
        }
121
122
        // Check if current database is enabled
123
        $database = $this->getSelectedDatabase();
124
        $enabledForDb = $this->preparedQuery(
125
            "SELECT is_fulltext_enabled FROM sys.databases WHERE name = ?",
126
            array($database)
127
        )->value();
128
        return $enabledForDb;
129
    }
130
131
    public function supportsCollations()
132
    {
133
        return true;
134
    }
135
136
    public function supportsTimezoneOverride()
137
    {
138
        return true;
139
    }
140
141
    public function getDatabaseServer()
142
    {
143
        return "sqlsrv";
144
    }
145
146
    public function selectDatabase($name, $create = false, $errorLevel = E_USER_ERROR)
147
    {
148
        $this->fullTextEnabled = null;
149
150
        return parent::selectDatabase($name, $create, $errorLevel);
151
    }
152
153
    public function clearTable($table)
154
    {
155
        $this->query("TRUNCATE TABLE \"$table\"");
156
    }
157
158
    /**
159
     * SQL Server uses CURRENT_TIMESTAMP for the current date/time.
160
     */
161
    public function now()
162
    {
163
        return 'CURRENT_TIMESTAMP';
164
    }
165
166
    /**
167
     * Returns the database-specific version of the random() function
168
     */
169
    public function random()
170
    {
171
        return 'RAND()';
172
    }
173
174
    /**
175
     * The core search engine configuration.
176
     * Picks up the fulltext-indexed tables from the database and executes search on all of them.
177
     * Results are obtained as ID-ClassName pairs which is later used to reconstruct the DataObjectSet.
178
     *
179
     * @param array $classesToSearch computes all descendants and includes them. Check is done via WHERE clause.
180
     * @param string $keywords Keywords as a space separated string
181
     * @param int $start
182
     * @param int $pageLength
183
     * @param string $sortBy
184
     * @param string $extraFilter
185
     * @param bool $booleanSearch
186
     * @param string $alternativeFileFilter
187
     * @param bool $invertedMatch
188
     * @return PaginatedList DataObjectSet of result pages
189
     */
190
    public function searchEngine($classesToSearch, $keywords, $start, $pageLength, $sortBy = "Relevance DESC", $extraFilter = "", $booleanSearch = false, $alternativeFileFilter = "", $invertedMatch = false)
191
    {
192
        $start = (int)$start;
193
        $pageLength = (int)$pageLength;
194
        $results = new ArrayList();
195
196
        if (!$this->fullTextEnabled()) {
197
            return new PaginatedList($results);
198
        }
199
        if (!in_array(substr($sortBy, 0, 9), array('"Relevanc', 'Relevance'))) {
200
            user_error("Non-relevance sort not supported.", E_USER_ERROR);
201
        }
202
203
        $allClassesToSearch = array();
204
        foreach ($classesToSearch as $class) {
205
            $allClassesToSearch = array_merge($allClassesToSearch, array_values(ClassInfo::dataClassesFor($class)));
206
        }
207
        $allClassesToSearch = array_unique($allClassesToSearch);
208
209
        //Get a list of all the tables and columns we'll be searching on:
210
        $fulltextColumns = $this->query('EXEC sp_help_fulltext_columns');
211
        $queries = array();
212
213
        // Sort the columns back into tables.
214
        $tables = array();
215
        foreach ($fulltextColumns as $column) {
216
            // Skip extension tables.
217
            if (substr($column['TABLE_NAME'], -5) == '_Live' || substr($column['TABLE_NAME'], -9) == '_versions') {
218
                continue;
219
            }
220
221
            // Add the column to table.
222
            $table = &$tables[$column['TABLE_NAME']];
223
            if (!$table) {
224
                $table = array($column['FULLTEXT_COLUMN_NAME']);
225
            } else {
226
                array_push($table, $column['FULLTEXT_COLUMN_NAME']);
227
            }
228
        }
229
230
        // Create one query per each table, $columns not used. We want just the ID and the ClassName of the object from this query.
231
        foreach ($tables as $tableName => $columns) {
232
            $class = DataObject::getSchema()->tableClass($tableName);
233
            $join = $this->fullTextSearchMSSQL($tableName, $keywords);
234
            if (!$join) {
235
                return new PaginatedList($results);
236
            } // avoid "Null or empty full-text predicate"
237
238
            // Check if we need to add ShowInSearch
239
            $where = null;
240
            if ($class === 'SilverStripe\\CMS\\Model\\SiteTree') {
241
                $where = array("\"$tableName\".\"ShowInSearch\"!=0");
242
            } elseif ($class === 'SilverStripe\\Assets\\File') {
243
                // File.ShowInSearch was added later, keep the database driver backwards compatible
244
                // by checking for its existence first
245
                $fields = $this->getSchemaManager()->fieldList($tableName);
246
                if (array_key_exists('ShowInSearch', $fields)) {
247
                    $where = array("\"$tableName\".\"ShowInSearch\"!=0");
248
                }
249
            }
250
251
            $queries[$tableName] = DataList::create($class)->where($where)->dataQuery()->query();
0 ignored issues
show
Bug introduced by
It seems like $where defined by null on line 239 can also be of type null; however, SilverStripe\ORM\DataList::where() does only seem to accept string|array|object<Silv...ries\SQLConditionGroup>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
252
            $queries[$tableName]->setOrderBy(array());
253
254
            // Join with CONTAINSTABLE, a full text searcher that includes relevance factor
255
            $queries[$tableName]->setFrom(array("\"$tableName\" INNER JOIN $join AS \"ft\" ON \"$tableName\".\"ID\"=\"ft\".\"KEY\""));
256
            // Join with the base class if needed, as we want to test agains the ClassName
257
            if ($tableName != $tableName) {
258
                $queries[$tableName]->setFrom("INNER JOIN \"$tableName\" ON  \"$tableName\".\"ID\"=\"$tableName\".\"ID\"");
259
            }
260
261
            $queries[$tableName]->setSelect(array("\"$tableName\".\"ID\""));
262
            $queries[$tableName]->selectField("'$tableName'", 'Source');
263
            $queries[$tableName]->selectField('Rank', 'Relevance');
264
            if ($extraFilter) {
265
                $queries[$tableName]->addWhere($extraFilter);
266
            }
267
            if (count($allClassesToSearch)) {
268
                $classesPlaceholder = DB::placeholders($allClassesToSearch);
269
                $queries[$tableName]->addWhere(array(
270
                    "\"$tableName\".\"ClassName\" IN ($classesPlaceholder)" =>
271
                    $allClassesToSearch
272
                ));
273
            }
274
            // Reset the parameters that would get in the way
275
        }
276
277
        // Generate SQL
278
        $querySQLs = array();
279
        $queryParameters = array();
280
        foreach ($queries as $query) {
281
            /** @var SQLSelect $query */
282
            $querySQLs[] = $query->sql($parameters);
283
            $queryParameters = array_merge($queryParameters, $parameters);
284
        }
285
286
        // Unite the SQL
287
        $fullQuery = implode(" UNION ", $querySQLs) . " ORDER BY $sortBy";
288
289
        // Perform the search
290
        $result = $this->preparedQuery($fullQuery, $queryParameters);
291
292
        // Regenerate DataObjectSet - watch out, numRecords doesn't work on sqlsrv driver on Windows.
293
        $current = -1;
294
        $objects = array();
295
        foreach ($result as $row) {
296
            $current++;
297
298
            // Select a subset for paging
299
            if ($current >= $start && $current < $start + $pageLength) {
300
                $objects[] = DataObject::get_by_id($row['Source'], $row['ID']);
301
            }
302
        }
303
304
        if (isset($objects)) {
305
            $results = new ArrayList($objects);
306
        } else {
307
            $results = new ArrayList();
308
        }
309
        $list = new PaginatedList($results);
310
        $list->setPageStart($start);
311
        $list->setPageLength($pageLength);
312
        $list->setTotalItems($current+1);
313
        return $list;
314
    }
315
316
    /**
317
     * Allow auto-increment primary key editing on the given table.
318
     * Some databases need to enable this specially.
319
     *
320
     * @param string $table The name of the table to have PK editing allowed on
321
     * @param bool $allow True to start, false to finish
322
     */
323
    public function allowPrimaryKeyEditing($table, $allow = true)
324
    {
325
        $this->query("SET IDENTITY_INSERT \"$table\" " . ($allow ? "ON" : "OFF"));
326
    }
327
328
    /**
329
     * Returns a SQL fragment for querying a fulltext search index
330
     *
331
     * @param string $tableName specific - table name
332
     * @param string $keywords The search query
333
     * @param array $fields The list of field names to search on, or null to include all
334
     * @return string Clause, or null if keyword set is empty or the string with JOIN clause to be added to SQL query
335
     */
336
    public function fullTextSearchMSSQL($tableName, $keywords, $fields = null)
337
    {
338
        // Make sure we are getting an array of fields
339
        if (isset($fields) && !is_array($fields)) {
340
            $fields = array($fields);
341
        }
342
343
        // Strip unfriendly characters, SQLServer "CONTAINS" predicate will crash on & and | and ignore others anyway.
344
        if (function_exists('mb_ereg_replace')) {
345
            $keywords = mb_ereg_replace('[^\w\s]', '', trim($keywords));
346
        } else {
347
            $keywords = $this->escapeString(str_replace(array('&', '|', '!', '"', '\''), '', trim($keywords)));
348
        }
349
350
        // Remove stopwords, concat with ANDs
351
        $keywordList = explode(' ', $keywords);
352
        $keywordList = $this->removeStopwords($keywordList);
353
354
        // remove any empty values from the array
355
        $keywordList = array_filter($keywordList);
356
        if (empty($keywordList)) {
357
            return null;
358
        }
359
360
        $keywords = implode(' AND ', $keywordList);
361
        if ($fields) {
362
            $fieldNames = '"' . implode('", "', $fields) . '"';
363
        } else {
364
            $fieldNames = "*";
365
        }
366
367
        return "CONTAINSTABLE(\"$tableName\", ($fieldNames), '$keywords')";
368
    }
369
370
    /**
371
     * Remove stopwords that would kill a MSSQL full-text query
372
     *
373
     * @param array $keywords
374
     *
375
     * @return array $keywords with stopwords removed
376
     */
377
    public function removeStopwords($keywords)
378
    {
379
        $goodKeywords = array();
380
        foreach ($keywords as $keyword) {
381
            if (in_array($keyword, self::$noiseWords)) {
382
                continue;
383
            }
384
            $goodKeywords[] = trim($keyword);
385
        }
386
        return $goodKeywords;
387
    }
388
389
    /**
390
     * Does this database support transactions?
391
     */
392
    public function supportsTransactions()
393
    {
394
        return $this->supportsTransactions;
395
    }
396
397
    /**
398
     * This is a quick lookup to discover if the database supports particular extensions
399
     * Currently, MSSQL supports no extensions
400
     *
401
     * @param array $extensions List of extensions to check for support of. The key of this array
402
     * will be an extension name, and the value the configuration for that extension. This
403
     * could be one of partitions, tablespaces, or clustering
404
     * @return boolean Flag indicating support for all of the above
405
     */
406
    public function supportsExtensions($extensions = array('partitions', 'tablespaces', 'clustering'))
407
    {
408
        if (isset($extensions['partitions'])) {
409
            return false;
410
        } elseif (isset($extensions['tablespaces'])) {
411
            return false;
412
        } elseif (isset($extensions['clustering'])) {
413
            return false;
414
        } else {
415
            return false;
416
        }
417
    }
418
419
    /**
420
     * Start transaction. READ ONLY not supported.
421
     *
422
     * @param bool $transactionMode
423
     * @param bool $sessionCharacteristics
424
     */
425
    public function transactionStart($transactionMode = false, $sessionCharacteristics = false)
426
    {
427 View Code Duplication
        if ($this->transactionNesting > 0) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
428
            $this->transactionSavepoint('NESTEDTRANSACTION' . $this->transactionNesting);
429
        } elseif ($this->connector instanceof SQLServerConnector) {
430
            $this->connector->transactionStart();
431
        } else {
432
            $this->query('BEGIN TRANSACTION');
433
        }
434
        ++$this->transactionNesting;
435
    }
436
437
    public function transactionSavepoint($savepoint)
438
    {
439
        $this->query("SAVE TRANSACTION \"$savepoint\"");
440
    }
441
442
    public function transactionRollback($savepoint = false)
443
    {
444
        // Named transaction
445
        if ($savepoint) {
446
            $this->query("ROLLBACK TRANSACTION \"$savepoint\"");
447
            return true;
448
        }
449
450
        // Fail if transaction isn't available
451
        if (!$this->transactionNesting) {
452
            return false;
453
        }
454
455
        --$this->transactionNesting;
456 View Code Duplication
        if ($this->transactionNesting > 0) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
457
            $this->transactionRollback('NESTEDTRANSACTION' . $this->transactionNesting);
0 ignored issues
show
Documentation introduced by
'NESTEDTRANSACTION' . $this->transactionNesting is of type string, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
458
        } elseif ($this->connector instanceof SQLServerConnector) {
459
            $this->connector->transactionRollback();
460
        } else {
461
            $this->query('ROLLBACK TRANSACTION');
462
        }
463
        return true;
464
    }
465
466
    public function transactionEnd($chain = false)
467
    {
468
        // Fail if transaction isn't available
469
        if (!$this->transactionNesting) {
470
            return false;
471
        }
472
        --$this->transactionNesting;
473 View Code Duplication
        if ($this->transactionNesting <= 0) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
474
            $this->transactionNesting = 0;
0 ignored issues
show
Documentation Bug introduced by
The property $transactionNesting was declared of type boolean, but 0 is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
475
            if ($this->connector instanceof SQLServerConnector) {
476
                $this->connector->transactionEnd();
477
            } else {
478
                $this->query('COMMIT TRANSACTION');
479
            }
480
        }
481
        return true;
482
    }
483
484
    /**
485
     * In error condition, set transactionNesting to zero
486
     */
487
    protected function resetTransactionNesting()
488
    {
489
        $this->transactionNesting = 0;
0 ignored issues
show
Documentation Bug introduced by
The property $transactionNesting was declared of type boolean, but 0 is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
490
    }
491
492
    public function query($sql, $errorLevel = E_USER_ERROR)
493
    {
494
        $this->inspectQuery($sql);
495
        return parent::query($sql, $errorLevel);
496
    }
497
498
    public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR)
499
    {
500
        $this->inspectQuery($sql);
501
        return parent::preparedQuery($sql, $parameters, $errorLevel);
502
    }
503
504
    protected function inspectQuery($sql)
505
    {
506
        // Any DDL discards transactions.
507
        $isDDL = $this->getConnector()->isQueryDDL($sql);
508
509
        if ($isDDL) {
510
            $this->resetTransactionNesting();
511
        }
512
    }
513
514
    public function comparisonClause($field, $value, $exact = false, $negate = false, $caseSensitive = null, $parameterised = false)
515
    {
516
        if ($exact) {
517
            $comp = ($negate) ? '!=' : '=';
518
        } else {
519
            $comp = 'LIKE';
520
            if ($negate) {
521
                $comp = 'NOT ' . $comp;
522
            }
523
        }
524
525
        // Field definitions are case insensitive by default,
526
        // change used collation for case sensitive searches.
527
        $collateClause = '';
528
        if ($caseSensitive === true) {
529
            if (self::get_collation()) {
530
                $collation = preg_replace('/_CI_/', '_CS_', self::get_collation());
531
            } else {
532
                $collation = 'Latin1_General_CS_AS';
533
            }
534
            $collateClause = ' COLLATE ' . $collation;
535
        } elseif ($caseSensitive === false) {
536
            if (self::get_collation()) {
537
                $collation = preg_replace('/_CS_/', '_CI_', self::get_collation());
538
            } else {
539
                $collation = 'Latin1_General_CI_AS';
540
            }
541
            $collateClause = ' COLLATE ' . $collation;
542
        }
543
544
        $clause = sprintf("%s %s %s", $field, $comp, $parameterised ? '?' : "'$value'");
545
        if ($collateClause) {
546
            $clause .= $collateClause;
547
        }
548
549
        return $clause;
550
    }
551
552
    /**
553
     * Function to return an SQL datetime expression for MSSQL
554
     * used for querying a datetime in a certain format
555
     *
556
     * @param string $date to be formated, can be either 'now', literal datetime like '1973-10-14 10:30:00' or field name, e.g. '"SiteTree"."Created"'
557
     * @param string $format to be used, supported specifiers:
558
     * %Y = Year (four digits)
559
     * %m = Month (01..12)
560
     * %d = Day (01..31)
561
     * %H = Hour (00..23)
562
     * %i = Minutes (00..59)
563
     * %s = Seconds (00..59)
564
     * %U = unix timestamp, can only be used on it's own
565
     * @return string SQL datetime expression to query for a formatted datetime
566
     */
567
    public function formattedDatetimeClause($date, $format)
568
    {
569
        preg_match_all('/%(.)/', $format, $matches);
570
        foreach ($matches[1] as $match) {
571
            if (array_search($match, array('Y', 'm', 'd', 'H', 'i', 's', 'U')) === false) {
572
                user_error('formattedDatetimeClause(): unsupported format character %' . $match, E_USER_WARNING);
573
            }
574
        }
575
576 View Code Duplication
        if (preg_match('/^now$/i', $date)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
577
            $date = "CURRENT_TIMESTAMP";
578
        } elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date)) {
579
            $date = "'$date.000'";
580
        }
581
582
        if ($format == '%U') {
583
            return "DATEDIFF(s, '1970-01-01 00:00:00', DATEADD(hour, DATEDIFF(hour, GETDATE(), GETUTCDATE()), $date))";
584
        }
585
586
        $trans = array(
587
            'Y' => 'yy',
588
            'm' => 'mm',
589
            'd' => 'dd',
590
            'H' => 'hh',
591
            'i' => 'mi',
592
            's' => 'ss',
593
        );
594
595
        $strings = array();
596
        $buffer = $format;
597
        while (strlen($buffer)) {
598
            if (substr($buffer, 0, 1) == '%') {
599
                $f = substr($buffer, 1, 1);
600
                $flen = $f == 'Y' ? 4 : 2;
601
                $strings[] = "RIGHT('0' + CAST(DATEPART({$trans[$f]},$date) AS VARCHAR), $flen)";
602
                $buffer = substr($buffer, 2);
603
            } else {
604
                $pos = strpos($buffer, '%');
605
                if ($pos === false) {
606
                    $strings[] = $buffer;
607
                    $buffer = '';
608
                } else {
609
                    $strings[] = "'".substr($buffer, 0, $pos)."'";
610
                    $buffer = substr($buffer, $pos);
611
                }
612
            }
613
        }
614
615
        return '(' . implode(' + ', $strings) . ')';
616
    }
617
618
    /**
619
     * Function to return an SQL datetime expression for MSSQL.
620
     * used for querying a datetime addition
621
     *
622
     * @param string $date, can be either 'now', literal datetime like '1973-10-14 10:30:00' or field name, e.g. '"SiteTree"."Created"'
0 ignored issues
show
Bug introduced by
There is no parameter named $date,. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
623
     * @param string $interval to be added, use the format [sign][integer] [qualifier], e.g. -1 Day, +15 minutes, +1 YEAR
624
     * supported qualifiers:
625
     * - years
626
     * - months
627
     * - days
628
     * - hours
629
     * - minutes
630
     * - seconds
631
     * This includes the singular forms as well
632
     * @return string SQL datetime expression to query for a datetime (YYYY-MM-DD hh:mm:ss) which is the result of the addition
633
     */
634
    public function datetimeIntervalClause($date, $interval)
635
    {
636
        $trans = array(
637
            'year' => 'yy',
638
            'month' => 'mm',
639
            'day' => 'dd',
640
            'hour' => 'hh',
641
            'minute' => 'mi',
642
            'second' => 'ss',
643
        );
644
645
        $singularinterval = preg_replace('/(year|month|day|hour|minute|second)s/i', '$1', $interval);
646
647
        if (
648
            !($params = preg_match('/([-+]\d+) (\w+)/i', $singularinterval, $matches)) ||
649
            !isset($trans[strtolower($matches[2])])
650
        ) {
651
            user_error('datetimeIntervalClause(): invalid interval ' . $interval, E_USER_WARNING);
652
        }
653
654 View Code Duplication
        if (preg_match('/^now$/i', $date)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
655
            $date = "CURRENT_TIMESTAMP";
656
        } elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date)) {
657
            $date = "'$date'";
658
        }
659
660
        return "CONVERT(VARCHAR, DATEADD(" . $trans[strtolower($matches[2])] . ", " . (int)$matches[1] . ", $date), 120)";
661
    }
662
663
    /**
664
     * Function to return an SQL datetime expression for MSSQL.
665
     * used for querying a datetime substraction
666
     *
667
     * @param string $date1, can be either 'now', literal datetime like '1973-10-14 10:30:00' or field name, e.g. '"SiteTree"."Created"'
0 ignored issues
show
Documentation introduced by
There is no parameter named $date1,. Did you maybe mean $date1?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
668
     * @param string $date2 to be substracted of $date1, can be either 'now', literal datetime like '1973-10-14 10:30:00' or field name, e.g. '"SiteTree"."Created"'
669
     * @return string SQL datetime expression to query for the interval between $date1 and $date2 in seconds which is the result of the substraction
670
     */
671
    public function datetimeDifferenceClause($date1, $date2)
672
    {
673 View Code Duplication
        if (preg_match('/^now$/i', $date1)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
674
            $date1 = "CURRENT_TIMESTAMP";
675
        } elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date1)) {
676
            $date1 = "'$date1'";
677
        }
678
679 View Code Duplication
        if (preg_match('/^now$/i', $date2)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
680
            $date2 = "CURRENT_TIMESTAMP";
681
        } elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date2)) {
682
            $date2 = "'$date2'";
683
        }
684
685
        return "DATEDIFF(s, $date2, $date1)";
686
    }
687
}
688