Completed
Push — master ( a34d67...6a25ac )
by Sam
02:12
created

MSSQLDatabase   F

Complexity

Total Complexity 89

Size/Duplication

Total Lines 605
Duplicated Lines 3.31 %

Coupling/Cohesion

Components 1
Dependencies 14

Importance

Changes 0
Metric Value
wmc 89
lcom 1
cbo 14
dl 20
loc 605
rs 2.6477
c 0
b 0
f 0

26 Methods

Rating   Name   Duplication   Size   Complexity  
A set_collation() 0 4 1
A connect() 0 8 1
A fullTextEnabled() 0 7 2
A getDatabaseServer() 0 4 1
A clearTable() 0 4 1
A now() 0 4 1
A random() 0 4 1
A allowPrimaryKeyEditing() 0 4 2
A supportsTransactions() 0 4 1
A supportsExtensions() 0 12 4
A transactionEnd() 0 8 2
D comparisonClause() 0 37 10
B datetimeDifferenceClause() 10 16 5
A get_collation() 0 4 1
A updateFullTextEnabled() 0 16 2
A supportsCollations() 0 4 1
A supportsTimezoneOverride() 0 4 1
A selectDatabase() 0 6 1
F searchEngine() 0 123 21
B fullTextSearchMSSQL() 0 33 6
A removeStopwords() 0 11 3
A transactionStart() 0 8 2
A transactionSavepoint() 0 4 1
A transactionRollback() 0 10 3
C formattedDatetimeClause() 5 50 10
B datetimeIntervalClause() 5 28 5

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like MSSQLDatabase often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MSSQLDatabase, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\MSSQL;
4
5
use SilverStripe\Core\Config\Config;
6
use SilverStripe\Core\ClassInfo;
7
use SilverStripe\ORM\ArrayList;
8
use SilverStripe\ORM\Connect\Database;
9
use SilverStripe\ORM\DataList;
10
use SilverStripe\ORM\DB;
11
use SilverStripe\ORM\DataObject;
12
use SilverStripe\ORM\PaginatedList;
13
use SilverStripe\ORM\Queries\SQLSelect;
14
15
/**
16
 * Microsoft SQL Server 2008+ connector class.
17
 *
18
 * <h2>Connecting using Windows</h2>
19
 *
20
 * If you've got your website running on Windows, it's highly recommended you
21
 * use Microsoft SQL Server Driver for PHP "sqlsrv".
22
 *
23
 * A complete guide to installing a Windows IIS + PHP + SQL Server web stack can be
24
 * found here: http://doc.silverstripe.org/installation-on-windows-server-manual-iis
25
 *
26
 * @see http://sqlsrvphp.codeplex.com/
27
 *
28
 * <h2>Connecting using Linux or Mac OS X</h2>
29
 *
30
 * The following commands assume you used the default package manager
31
 * to install PHP with the operating system.
32
 *
33
 * Debian, and Ubuntu:
34
 * <code>apt-get install php5-sybase</code>
35
 *
36
 * Fedora, CentOS and RedHat:
37
 * <code>yum install php-mssql</code>
38
 *
39
 * Mac OS X (MacPorts):
40
 * <code>port install php5-mssql</code>
41
 *
42
 * These packages will install the mssql extension for PHP, as well
43
 * as FreeTDS, which will let you connect to SQL Server.
44
 *
45
 * More information available in the SilverStripe developer wiki:
46
 * @see http://doc.silverstripe.org/modules:mssql
47
 * @see http://doc.silverstripe.org/installation-on-windows-server-manual-iis
48
 *
49
 * References:
50
 * @see http://freetds.org
51
 */
52
class MSSQLDatabase extends Database
53
{
54
55
    /**
56
     * Words that will trigger an error if passed to a SQL Server fulltext search
57
     */
58
    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');
59
60
    /**
61
     * Transactions will work with FreeTDS, but not entirely with sqlsrv driver on Windows with MARS enabled.
62
     * TODO:
63
     * - after the test fails with open transaction, the transaction should be rolled back,
64
     *   otherwise other tests will break claiming that transaction is still open.
65
     * - figure out SAVEPOINTS
66
     * - READ ONLY transactions
67
     */
68
    protected $supportsTransactions = true;
69
70
    /**
71
     * Cached flag to determine if full-text is enabled. This is set by
72
     * {@link MSSQLDatabase::fullTextEnabled()}
73
     *
74
     * @var boolean
75
     */
76
    protected $fullTextEnabled = null;
77
78
    /**
79
     * Set the default collation of the MSSQL nvarchar fields that we create.
80
     * We don't apply this to the database as a whole, so that we can use unicode collations.
81
     *
82
     * @param string $collation
83
     */
84
    public static function set_collation($collation)
85
    {
86
        Config::inst()->update('SilverStripe\\MSSQL\\MSSQLDatabase', 'collation', $collation);
87
    }
88
89
    /**
90
     * The default collation of the MSSQL nvarchar fields that we create.
91
     * We don't apply this to the database as a whole, so that we can use
92
     * unicode collations.
93
     *
94
     * @return string
95
     */
96
    public static function get_collation()
97
    {
98
        return Config::inst()->get('SilverStripe\\MSSQL\\MSSQLDatabase', 'collation');
99
    }
100
101
    /**
102
     * Connect to a MS SQL database.
103
     * @param array $parameters An map of parameters, which should include:
104
     *  - server: The server, eg, localhost
105
     *  - username: The username to log on with
106
     *  - password: The password to log on with
107
     *  - database: The database to connect to
108
     *  - windowsauthentication: Set to true to use windows authentication
109
     *    instead of username/password
110
     */
111
    public function connect($parameters)
112
    {
113
        parent::connect($parameters);
114
115
        // Configure the connection
116
        $this->query('SET QUOTED_IDENTIFIER ON');
117
        $this->query('SET TEXTSIZE 2147483647');
118
    }
119
120
    /**
121
     * Checks whether the current SQL Server version has full-text
122
     * support installed and full-text is enabled for this database.
123
     *
124
     * @return boolean
125
     */
126
    public function fullTextEnabled()
127
    {
128
        if ($this->fullTextEnabled === null) {
129
            $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...
130
        }
131
        return $this->fullTextEnabled;
132
    }
133
134
    /**
135
     * Checks whether the current SQL Server version has full-text
136
     * support installed and full-text is enabled for this database.
137
     *
138
     * @return boolean
139
     */
140
    protected function updateFullTextEnabled()
141
    {
142
        // Check if installed
143
        $isInstalled = $this->query("SELECT fulltextserviceproperty('isfulltextinstalled')")->value();
144
        if (!$isInstalled) {
145
            return false;
146
        }
147
148
        // Check if current database is enabled
149
        $database = $this->getSelectedDatabase();
150
        $enabledForDb = $this->preparedQuery(
151
            "SELECT is_fulltext_enabled FROM sys.databases WHERE name = ?",
152
            array($database)
153
        )->value();
154
        return $enabledForDb;
155
    }
156
157
    public function supportsCollations()
158
    {
159
        return true;
160
    }
161
162
    public function supportsTimezoneOverride()
163
    {
164
        return true;
165
    }
166
167
    public function getDatabaseServer()
168
    {
169
        return "sqlsrv";
170
    }
171
172
    public function selectDatabase($name, $create = false, $errorLevel = E_USER_ERROR)
173
    {
174
        $this->fullTextEnabled = null;
175
176
        return parent::selectDatabase($name, $create, $errorLevel);
177
    }
178
179
    public function clearTable($table)
180
    {
181
        $this->query("TRUNCATE TABLE \"$table\"");
182
    }
183
184
    /**
185
     * SQL Server uses CURRENT_TIMESTAMP for the current date/time.
186
     */
187
    public function now()
188
    {
189
        return 'CURRENT_TIMESTAMP';
190
    }
191
192
    /**
193
     * Returns the database-specific version of the random() function
194
     */
195
    public function random()
196
    {
197
        return 'RAND()';
198
    }
199
200
    /**
201
     * The core search engine configuration.
202
     * Picks up the fulltext-indexed tables from the database and executes search on all of them.
203
     * Results are obtained as ID-ClassName pairs which is later used to reconstruct the DataObjectSet.
204
     *
205
     * @param array $classesToSearch computes all descendants and includes them. Check is done via WHERE clause.
206
     * @param string $keywords Keywords as a space separated string
207
     * @param int $start
208
     * @param int $pageLength
209
     * @param string $sortBy
210
     * @param string $extraFilter
211
     * @param bool $booleanSearch
212
     * @param string $alternativeFileFilter
213
     * @param bool $invertedMatch
214
     * @return PaginatedList DataObjectSet of result pages
215
     */
216
    public function searchEngine($classesToSearch, $keywords, $start, $pageLength, $sortBy = "Relevance DESC", $extraFilter = "", $booleanSearch = false, $alternativeFileFilter = "", $invertedMatch = false)
217
    {
218
        $results = new ArrayList();
219
220
        if (!$this->fullTextEnabled()) {
221
            return new PaginatedList($results);
222
        }
223
        if (!in_array(substr($sortBy, 0, 9), array('"Relevanc', 'Relevance'))) {
224
            user_error("Non-relevance sort not supported.", E_USER_ERROR);
225
        }
226
227
        $allClassesToSearch = array();
228
        foreach ($classesToSearch as $class) {
229
            $allClassesToSearch = array_merge($allClassesToSearch, array_values(ClassInfo::dataClassesFor($class)));
230
        }
231
        $allClassesToSearch = array_unique($allClassesToSearch);
232
233
        //Get a list of all the tables and columns we'll be searching on:
234
        $fulltextColumns = $this->query('EXEC sp_help_fulltext_columns');
235
        $queries = array();
236
237
        // Sort the columns back into tables.
238
        $tables = array();
239
        foreach ($fulltextColumns as $column) {
240
            // Skip extension tables.
241
            if (substr($column['TABLE_NAME'], -5) == '_Live' || substr($column['TABLE_NAME'], -9) == '_versions') {
242
                continue;
243
            }
244
245
            // Add the column to table.
246
            $table = &$tables[$column['TABLE_NAME']];
247
            if (!$table) {
248
                $table = array($column['FULLTEXT_COLUMN_NAME']);
249
            } else {
250
                array_push($table, $column['FULLTEXT_COLUMN_NAME']);
251
            }
252
        }
253
254
        // Create one query per each table, $columns not used. We want just the ID and the ClassName of the object from this query.
255
        foreach ($tables as $tableName => $columns) {
256
            $class = DataObject::getSchema()->tableClass($tableName);
257
            $join = $this->fullTextSearchMSSQL($tableName, $keywords);
258
            if (!$join) {
259
                return new PaginatedList($results);
260
            } // avoid "Null or empty full-text predicate"
261
262
            // Check if we need to add ShowInSearch
263
            $where = null;
264
            if ($class === 'SilverStripe\\CMS\\Model\\SiteTree') {
265
                $where = array("\"$tableName\".\"ShowInSearch\"!=0");
266
            } elseif ($class === 'SilverStripe\\Assets\\File') {
267
                // File.ShowInSearch was added later, keep the database driver backwards compatible
268
                // by checking for its existence first
269
                $fields = $this->getSchemaManager()->fieldList($tableName);
270
                if (array_key_exists('ShowInSearch', $fields)) {
271
                    $where = array("\"$tableName\".\"ShowInSearch\"!=0");
272
                }
273
            }
274
275
            $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 263 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...
276
            $queries[$tableName]->setOrderBy(array());
277
278
            // Join with CONTAINSTABLE, a full text searcher that includes relevance factor
279
            $queries[$tableName]->setFrom(array("\"$tableName\" INNER JOIN $join AS \"ft\" ON \"$tableName\".\"ID\"=\"ft\".\"KEY\""));
280
            // Join with the base class if needed, as we want to test agains the ClassName
281
            if ($tableName != $tableName) {
282
                $queries[$tableName]->setFrom("INNER JOIN \"$tableName\" ON  \"$tableName\".\"ID\"=\"$tableName\".\"ID\"");
283
            }
284
285
            $queries[$tableName]->setSelect(array("\"$tableName\".\"ID\""));
286
            $queries[$tableName]->selectField("'$tableName'", 'Source');
287
            $queries[$tableName]->selectField('Rank', 'Relevance');
288
            if ($extraFilter) {
289
                $queries[$tableName]->addWhere($extraFilter);
290
            }
291
            if (count($allClassesToSearch)) {
292
                $classesPlaceholder = DB::placeholders($allClassesToSearch);
293
                $queries[$tableName]->addWhere(array(
294
                    "\"$tableName\".\"ClassName\" IN ($classesPlaceholder)" =>
295
                    $allClassesToSearch
296
                ));
297
            }
298
            // Reset the parameters that would get in the way
299
        }
300
301
        // Generate SQL
302
        $querySQLs = array();
303
        $queryParameters = array();
304
        foreach ($queries as $query) {
305
            /** @var SQLSelect $query */
306
            $querySQLs[] = $query->sql($parameters);
307
            $queryParameters = array_merge($queryParameters, $parameters);
308
        }
309
310
        // Unite the SQL
311
        $fullQuery = implode(" UNION ", $querySQLs) . " ORDER BY $sortBy";
312
313
        // Perform the search
314
        $result = $this->preparedQuery($fullQuery, $queryParameters);
315
316
        // Regenerate DataObjectSet - watch out, numRecords doesn't work on sqlsrv driver on Windows.
317
        $current = -1;
318
        $objects = array();
319
        foreach ($result as $row) {
320
            $current++;
321
322
            // Select a subset for paging
323
            if ($current >= $start && $current < $start + $pageLength) {
324
                $objects[] = DataObject::get_by_id($row['Source'], $row['ID']);
325
            }
326
        }
327
328
        if (isset($objects)) {
329
            $results = new ArrayList($objects);
330
        } else {
331
            $results = new ArrayList();
332
        }
333
        $list = new PaginatedList($results);
334
        $list->setPageStart($start);
335
        $list->setPageLength($pageLength);
336
        $list->setTotalItems($current+1);
337
        return $list;
338
    }
339
340
    /**
341
     * Allow auto-increment primary key editing on the given table.
342
     * Some databases need to enable this specially.
343
     *
344
     * @param string $table The name of the table to have PK editing allowed on
345
     * @param bool $allow True to start, false to finish
346
     */
347
    public function allowPrimaryKeyEditing($table, $allow = true)
348
    {
349
        $this->query("SET IDENTITY_INSERT \"$table\" " . ($allow ? "ON" : "OFF"));
350
    }
351
352
    /**
353
     * Returns a SQL fragment for querying a fulltext search index
354
     *
355
     * @param string $tableName specific - table name
356
     * @param string $keywords The search query
357
     * @param array $fields The list of field names to search on, or null to include all
358
     * @return string Clause, or null if keyword set is empty or the string with JOIN clause to be added to SQL query
359
     */
360
    public function fullTextSearchMSSQL($tableName, $keywords, $fields = null)
361
    {
362
        // Make sure we are getting an array of fields
363
        if (isset($fields) && !is_array($fields)) {
364
            $fields = array($fields);
365
        }
366
367
        // Strip unfriendly characters, SQLServer "CONTAINS" predicate will crash on & and | and ignore others anyway.
368
        if (function_exists('mb_ereg_replace')) {
369
            $keywords = mb_ereg_replace('[^\w\s]', '', trim($keywords));
370
        } else {
371
            $keywords = $this->escapeString(str_replace(array('&', '|', '!', '"', '\''), '', trim($keywords)));
372
        }
373
374
        // Remove stopwords, concat with ANDs
375
        $keywordList = explode(' ', $keywords);
376
        $keywordList = $this->removeStopwords($keywordList);
377
378
        // remove any empty values from the array
379
        $keywordList = array_filter($keywordList);
380
        if (empty($keywordList)) {
381
            return null;
382
        }
383
384
        $keywords = implode(' AND ', $keywordList);
385
        if ($fields) {
386
            $fieldNames = '"' . implode('", "', $fields) . '"';
387
        } else {
388
            $fieldNames = "*";
389
        }
390
391
        return "CONTAINSTABLE(\"$tableName\", ($fieldNames), '$keywords')";
392
    }
393
394
    /**
395
     * Remove stopwords that would kill a MSSQL full-text query
396
     *
397
     * @param array $keywords
398
     *
399
     * @return array $keywords with stopwords removed
400
     */
401
    public function removeStopwords($keywords)
402
    {
403
        $goodKeywords = array();
404
        foreach ($keywords as $keyword) {
405
            if (in_array($keyword, self::$noiseWords)) {
406
                continue;
407
            }
408
            $goodKeywords[] = trim($keyword);
409
        }
410
        return $goodKeywords;
411
    }
412
413
    /**
414
     * Does this database support transactions?
415
     */
416
    public function supportsTransactions()
417
    {
418
        return $this->supportsTransactions;
419
    }
420
421
    /**
422
     * This is a quick lookup to discover if the database supports particular extensions
423
     * Currently, MSSQL supports no extensions
424
     *
425
     * @param array $extensions List of extensions to check for support of. The key of this array
426
     * will be an extension name, and the value the configuration for that extension. This
427
     * could be one of partitions, tablespaces, or clustering
428
     * @return boolean Flag indicating support for all of the above
429
     */
430
    public function supportsExtensions($extensions = array('partitions', 'tablespaces', 'clustering'))
431
    {
432
        if (isset($extensions['partitions'])) {
433
            return false;
434
        } elseif (isset($extensions['tablespaces'])) {
435
            return false;
436
        } elseif (isset($extensions['clustering'])) {
437
            return false;
438
        } else {
439
            return false;
440
        }
441
    }
442
443
    /**
444
     * Start transaction. READ ONLY not supported.
445
     *
446
     * @param bool $transactionMode
447
     * @param bool $sessionCharacteristics
448
     */
449
    public function transactionStart($transactionMode = false, $sessionCharacteristics = false)
450
    {
451
        if ($this->connector instanceof SQLServerConnector) {
452
            $this->connector->transactionStart();
453
        } else {
454
            $this->query('BEGIN TRANSACTION');
455
        }
456
    }
457
458
    public function transactionSavepoint($savepoint)
459
    {
460
        $this->query("SAVE TRANSACTION \"$savepoint\"");
461
    }
462
463
    public function transactionRollback($savepoint = false)
464
    {
465
        if ($savepoint) {
466
            $this->query("ROLLBACK TRANSACTION \"$savepoint\"");
467
        } elseif ($this->connector instanceof SQLServerConnector) {
468
            $this->connector->transactionRollback();
469
        } else {
470
            $this->query('ROLLBACK TRANSACTION');
471
        }
472
    }
473
474
    public function transactionEnd($chain = false)
475
    {
476
        if ($this->connector instanceof SQLServerConnector) {
477
            $this->connector->transactionEnd();
478
        } else {
479
            $this->query('COMMIT TRANSACTION');
480
        }
481
    }
482
483
    public function comparisonClause($field, $value, $exact = false, $negate = false, $caseSensitive = null, $parameterised = false)
484
    {
485
        if ($exact) {
486
            $comp = ($negate) ? '!=' : '=';
487
        } else {
488
            $comp = 'LIKE';
489
            if ($negate) {
490
                $comp = 'NOT ' . $comp;
491
            }
492
        }
493
494
        // Field definitions are case insensitive by default,
495
        // change used collation for case sensitive searches.
496
        $collateClause = '';
497
        if ($caseSensitive === true) {
498
            if (self::get_collation()) {
499
                $collation = preg_replace('/_CI_/', '_CS_', self::get_collation());
500
            } else {
501
                $collation = 'Latin1_General_CS_AS';
502
            }
503
            $collateClause = ' COLLATE ' . $collation;
504
        } elseif ($caseSensitive === false) {
505
            if (self::get_collation()) {
506
                $collation = preg_replace('/_CS_/', '_CI_', self::get_collation());
507
            } else {
508
                $collation = 'Latin1_General_CI_AS';
509
            }
510
            $collateClause = ' COLLATE ' . $collation;
511
        }
512
513
        $clause = sprintf("%s %s %s", $field, $comp, $parameterised ? '?' : "'$value'");
514
        if ($collateClause) {
515
            $clause .= $collateClause;
516
        }
517
518
        return $clause;
519
    }
520
521
    /**
522
     * Function to return an SQL datetime expression for MSSQL
523
     * used for querying a datetime in a certain format
524
     *
525
     * @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"'
526
     * @param string $format to be used, supported specifiers:
527
     * %Y = Year (four digits)
528
     * %m = Month (01..12)
529
     * %d = Day (01..31)
530
     * %H = Hour (00..23)
531
     * %i = Minutes (00..59)
532
     * %s = Seconds (00..59)
533
     * %U = unix timestamp, can only be used on it's own
534
     * @return string SQL datetime expression to query for a formatted datetime
535
     */
536
    public function formattedDatetimeClause($date, $format)
537
    {
538
        preg_match_all('/%(.)/', $format, $matches);
539
        foreach ($matches[1] as $match) {
540
            if (array_search($match, array('Y', 'm', 'd', 'H', 'i', 's', 'U')) === false) {
541
                user_error('formattedDatetimeClause(): unsupported format character %' . $match, E_USER_WARNING);
542
            }
543
        }
544
545 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...
546
            $date = "CURRENT_TIMESTAMP";
547
        } elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date)) {
548
            $date = "'$date.000'";
549
        }
550
551
        if ($format == '%U') {
552
            return "DATEDIFF(s, '1970-01-01 00:00:00', DATEADD(hour, DATEDIFF(hour, GETDATE(), GETUTCDATE()), $date))";
553
        }
554
555
        $trans = array(
556
            'Y' => 'yy',
557
            'm' => 'mm',
558
            'd' => 'dd',
559
            'H' => 'hh',
560
            'i' => 'mi',
561
            's' => 'ss',
562
        );
563
564
        $strings = array();
565
        $buffer = $format;
566
        while (strlen($buffer)) {
567
            if (substr($buffer, 0, 1) == '%') {
568
                $f = substr($buffer, 1, 1);
569
                $flen = $f == 'Y' ? 4 : 2;
570
                $strings[] = "RIGHT('0' + CAST(DATEPART({$trans[$f]},$date) AS VARCHAR), $flen)";
571
                $buffer = substr($buffer, 2);
572
            } else {
573
                $pos = strpos($buffer, '%');
574
                if ($pos === false) {
575
                    $strings[] = $buffer;
576
                    $buffer = '';
577
                } else {
578
                    $strings[] = "'".substr($buffer, 0, $pos)."'";
579
                    $buffer = substr($buffer, $pos);
580
                }
581
            }
582
        }
583
584
        return '(' . implode(' + ', $strings) . ')';
585
    }
586
587
    /**
588
     * Function to return an SQL datetime expression for MSSQL.
589
     * used for querying a datetime addition
590
     *
591
     * @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...
592
     * @param string $interval to be added, use the format [sign][integer] [qualifier], e.g. -1 Day, +15 minutes, +1 YEAR
593
     * supported qualifiers:
594
     * - years
595
     * - months
596
     * - days
597
     * - hours
598
     * - minutes
599
     * - seconds
600
     * This includes the singular forms as well
601
     * @return string SQL datetime expression to query for a datetime (YYYY-MM-DD hh:mm:ss) which is the result of the addition
602
     */
603
    public function datetimeIntervalClause($date, $interval)
604
    {
605
        $trans = array(
606
            'year' => 'yy',
607
            'month' => 'mm',
608
            'day' => 'dd',
609
            'hour' => 'hh',
610
            'minute' => 'mi',
611
            'second' => 'ss',
612
        );
613
614
        $singularinterval = preg_replace('/(year|month|day|hour|minute|second)s/i', '$1', $interval);
615
616
        if (
617
            !($params = preg_match('/([-+]\d+) (\w+)/i', $singularinterval, $matches)) ||
618
            !isset($trans[strtolower($matches[2])])
619
        ) {
620
            user_error('datetimeIntervalClause(): invalid interval ' . $interval, E_USER_WARNING);
621
        }
622
623 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...
624
            $date = "CURRENT_TIMESTAMP";
625
        } elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date)) {
626
            $date = "'$date'";
627
        }
628
629
        return "CONVERT(VARCHAR, DATEADD(" . $trans[strtolower($matches[2])] . ", " . (int)$matches[1] . ", $date), 120)";
630
    }
631
632
    /**
633
     * Function to return an SQL datetime expression for MSSQL.
634
     * used for querying a datetime substraction
635
     *
636
     * @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...
637
     * @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"'
638
     * @return string SQL datetime expression to query for the interval between $date1 and $date2 in seconds which is the result of the substraction
639
     */
640
    public function datetimeDifferenceClause($date1, $date2)
641
    {
642 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...
643
            $date1 = "CURRENT_TIMESTAMP";
644
        } elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date1)) {
645
            $date1 = "'$date1'";
646
        }
647
648 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...
649
            $date2 = "CURRENT_TIMESTAMP";
650
        } elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date2)) {
651
            $date2 = "'$date2'";
652
        }
653
654
        return "DATEDIFF(s, $date2, $date1)";
655
    }
656
}
657