Completed
Pull Request — master (#31)
by Damian
06:15
created

MSSQLDatabase::formattedDatetimeClause()   C

Complexity

Conditions 10
Paths 54

Size

Total Lines 50
Code Lines 35

Duplication

Lines 5
Ratio 10 %
Metric Value
dl 5
loc 50
rs 5.7647
cc 10
eloc 35
nc 54
nop 2

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace SilverStripe\MSSQL;
4
5
use SilverStripe\ORM\ArrayList;
6
use SilverStripe\ORM\DataList;
7
use SilverStripe\ORM\DB;
8
use SilverStripe\ORM\DataObject;
9
use SilverStripe\ORM\Connect\SS_Database;
10
use Config;
11
use ClassInfo;
12
use 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
 * @package mssql
53
 */
54
class MSSQLDatabase extends SS_Database
55
{
56
57
    /**
58
     * Words that will trigger an error if passed to a SQL Server fulltext search
59
     */
60
    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');
61
62
    /**
63
     * Transactions will work with FreeTDS, but not entirely with sqlsrv driver on Windows with MARS enabled.
64
     * TODO:
65
     * - after the test fails with open transaction, the transaction should be rolled back,
66
     *   otherwise other tests will break claiming that transaction is still open.
67
     * - figure out SAVEPOINTS
68
     * - READ ONLY transactions
69
     */
70
    protected $supportsTransactions = true;
71
72
    /**
73
     * Cached flag to determine if full-text is enabled. This is set by
74
     * {@link MSSQLDatabase::fullTextEnabled()}
75
     *
76
     * @var boolean
77
     */
78
    protected $fullTextEnabled = null;
79
80
    /**
81
     * Set the default collation of the MSSQL nvarchar fields that we create.
82
     * We don't apply this to the database as a whole, so that we can use unicode collations.
83
     *
84
     * @param string $collation
85
     */
86
    public static function set_collation($collation)
87
    {
88
        Config::inst()->update('MSSQLDatabase', 'collation', $collation);
89
    }
90
91
    /**
92
     * The default collation of the MSSQL nvarchar fields that we create.
93
     * We don't apply this to the database as a whole, so that we can use
94
     * unicode collations.
95
     *
96
     * @return string
97
     */
98
    public static function get_collation()
99
    {
100
        return Config::inst()->get('MSSQLDatabase', 'collation');
101
    }
102
103
    /**
104
     * Connect to a MS SQL database.
105
     * @param array $parameters An map of parameters, which should include:
106
     *  - server: The server, eg, localhost
107
     *  - username: The username to log on with
108
     *  - password: The password to log on with
109
     *  - database: The database to connect to
110
     *  - windowsauthentication: Set to true to use windows authentication
111
     *    instead of username/password
112
     */
113
    public function connect($parameters)
114
    {
115
        parent::connect($parameters);
116
117
        // Configure the connection
118
        $this->query('SET QUOTED_IDENTIFIER ON');
119
        $this->query('SET TEXTSIZE 2147483647');
120
    }
121
122
    /**
123
     * Checks whether the current SQL Server version has full-text
124
     * support installed and full-text is enabled for this database.
125
     *
126
     * @return boolean
127
     */
128
    public function fullTextEnabled()
129
    {
130
        if ($this->fullTextEnabled === null) {
131
            $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...
132
        }
133
        return $this->fullTextEnabled;
134
    }
135
136
    /**
137
     * Checks whether the current SQL Server version has full-text
138
     * support installed and full-text is enabled for this database.
139
     *
140
     * @return boolean
141
     */
142
    protected function updateFullTextEnabled()
143
    {
144
        // Check if installed
145
        $isInstalled = $this->query("SELECT fulltextserviceproperty('isfulltextinstalled')")->value();
146
        if (!$isInstalled) {
147
            return false;
148
        }
149
150
        // Check if current database is enabled
151
        $database = $this->getSelectedDatabase();
152
        $enabledForDb = $this->preparedQuery(
153
            "SELECT is_fulltext_enabled FROM sys.databases WHERE name = ?",
154
            array($database)
155
        )->value();
156
        return $enabledForDb;
157
    }
158
159
    public function supportsCollations()
160
    {
161
        return true;
162
    }
163
164
    public function supportsTimezoneOverride()
165
    {
166
        return true;
167
    }
168
169
    public function getDatabaseServer()
170
    {
171
        return "sqlsrv";
172
    }
173
174
    public function selectDatabase($name, $create = false, $errorLevel = E_USER_ERROR)
175
    {
176
        $this->fullTextEnabled = null;
177
178
        return parent::selectDatabase($name, $create, $errorLevel);
179
    }
180
181
    public function clearTable($table)
182
    {
183
        $this->query("TRUNCATE TABLE \"$table\"");
184
    }
185
186
    /**
187
     * SQL Server uses CURRENT_TIMESTAMP for the current date/time.
188
     */
189
    public function now()
190
    {
191
        return 'CURRENT_TIMESTAMP';
192
    }
193
194
    /**
195
     * Returns the database-specific version of the random() function
196
     */
197
    public function random()
198
    {
199
        return 'RAND()';
200
    }
201
202
    /**
203
     * The core search engine configuration.
204
     * Picks up the fulltext-indexed tables from the database and executes search on all of them.
205
     * Results are obtained as ID-ClassName pairs which is later used to reconstruct the DataObjectSet.
206
     *
207
     * @param array $classesToSearch computes all descendants and includes them. Check is done via WHERE clause.
208
     * @param string $keywords Keywords as a space separated string
209
     * @param int $start
210
     * @param int $pageLength
211
     * @param string $sortBy
212
     * @param string $extraFilter
213
     * @param bool $booleanSearch
214
     * @param string $alternativeFileFilter
215
     * @param bool $invertedMatch
216
     * @return PaginatedList DataObjectSet of result pages
217
     */
218
    public function searchEngine($classesToSearch, $keywords, $start, $pageLength, $sortBy = "Relevance DESC", $extraFilter = "", $booleanSearch = false, $alternativeFileFilter = "", $invertedMatch = false)
219
    {
220
        if (isset($objects)) {
0 ignored issues
show
Bug introduced by
The variable $objects seems only to be defined at a later point. As such the call to isset() seems to always evaluate to false.

This check marks calls to isset(...) or empty(...) that are found before the variable itself is defined. These will always have the same result.

This is likely the result of code being shifted around. Consider removing these calls.

Loading history...
221
            $results = new ArrayList($objects);
222
        } else {
223
            $results = new ArrayList();
224
        }
225
226
        if (!$this->fullTextEnabled()) {
227
            return $results;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $results; (SilverStripe\ORM\ArrayList) is incompatible with the return type declared by the abstract method SilverStripe\ORM\Connect\SS_Database::searchEngine of type SilverStripe\ORM\Connect\PaginatedList.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

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

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
228
        }
229
        if (!in_array(substr($sortBy, 0, 9), array('"Relevanc', 'Relevance'))) {
230
            user_error("Non-relevance sort not supported.", E_USER_ERROR);
231
        }
232
233
        $allClassesToSearch = array();
234
        foreach ($classesToSearch as $class) {
235
            $allClassesToSearch = array_merge($allClassesToSearch, array_values(ClassInfo::dataClassesFor($class)));
236
        }
237
        $allClassesToSearch = array_unique($allClassesToSearch);
238
239
        //Get a list of all the tables and columns we'll be searching on:
240
        $fulltextColumns = $this->query('EXEC sp_help_fulltext_columns');
241
        $queries = array();
242
243
        // Sort the columns back into tables.
244
        $tables = array();
245
        foreach ($fulltextColumns as $column) {
246
            // Skip extension tables.
247
            if (substr($column['TABLE_NAME'], -5) == '_Live' || substr($column['TABLE_NAME'], -9) == '_versions') {
248
                continue;
249
            }
250
251
            // Add the column to table.
252
            $table = &$tables[$column['TABLE_NAME']];
253
            if (!$table) {
254
                $table = array($column['FULLTEXT_COLUMN_NAME']);
255
            } else {
256
                array_push($table, $column['FULLTEXT_COLUMN_NAME']);
257
            }
258
        }
259
260
        // Create one query per each table, $columns not used. We want just the ID and the ClassName of the object from this query.
261
        foreach ($tables as $tableName => $columns) {
262
            $class = DataObject::getSchema()->tableClass($tableName);
263
            $join = $this->fullTextSearchMSSQL($tableName, $keywords);
264
            if (!$join) {
265
                return $results;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $results; (SilverStripe\ORM\ArrayList) is incompatible with the return type declared by the abstract method SilverStripe\ORM\Connect\SS_Database::searchEngine of type SilverStripe\ORM\Connect\PaginatedList.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

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

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
266
            } // avoid "Null or empty full-text predicate"
267
268
            // Check if we need to add ShowInSearch
269
            $where = null;
270
            if ($class === 'SiteTree') {
271
                $where = array("\"$tableName\".\"ShowInSearch\"!=0");
272
            } elseif ($class === 'File') {
273
                // File.ShowInSearch was added later, keep the database driver backwards compatible
274
                // by checking for its existence first
275
                $fields = $this->getSchemaManager()->fieldList($tableName);
276
                if (array_key_exists('ShowInSearch', $fields)) {
277
                    $where = array("\"$tableName\".\"ShowInSearch\"!=0");
278
                }
279
            }
280
281
            $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 269 can also be of type null; however, SilverStripe\ORM\DataList::where() does only seem to accept string|array|object<Silv...\ORM\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...
282
            $queries[$tableName]->setOrderBy(array());
283
284
            // Join with CONTAINSTABLE, a full text searcher that includes relevance factor
285
            $queries[$tableName]->setFrom(array("\"$tableName\" INNER JOIN $join AS \"ft\" ON \"$tableName\".\"ID\"=\"ft\".\"KEY\""));
286
            // Join with the base class if needed, as we want to test agains the ClassName
287
            if ($tableName != $tableName) {
288
                $queries[$tableName]->setFrom("INNER JOIN \"$tableName\" ON  \"$tableName\".\"ID\"=\"$tableName\".\"ID\"");
289
            }
290
291
            $queries[$tableName]->setSelect(array("\"$tableName\".\"ID\""));
292
            $queries[$tableName]->selectField("'$tableName'", 'Source');
293
            $queries[$tableName]->selectField('Rank', 'Relevance');
294
            if ($extraFilter) {
295
                $queries[$tableName]->addWhere($extraFilter);
296
            }
297
            if (count($allClassesToSearch)) {
298
                $classesPlaceholder = DB::placeholders($allClassesToSearch);
299
                $queries[$tableName]->addWhere(array(
300
                    "\"$tableName\".\"ClassName\" IN ($classesPlaceholder)" =>
301
                    $allClassesToSearch
302
                ));
303
            }
304
            // Reset the parameters that would get in the way
305
        }
306
307
        // Generate SQL
308
        $querySQLs = array();
309
        $queryParameters = array();
310
        foreach ($queries as $query) {
311
            /** @var SQLSelect $query */
312
            $querySQLs[] = $query->sql($parameters);
313
            $queryParameters = array_merge($queryParameters, $parameters);
314
        }
315
316
        // Unite the SQL
317
        $fullQuery = implode(" UNION ", $querySQLs) . " ORDER BY $sortBy";
318
319
        // Perform the search
320
        $result = $this->preparedQuery($fullQuery, $queryParameters);
321
322
        // Regenerate DataObjectSet - watch out, numRecords doesn't work on sqlsrv driver on Windows.
323
        $current = -1;
324
        $objects = array();
325
        foreach ($result as $row) {
326
            $current++;
327
328
            // Select a subset for paging
329
            if ($current >= $start && $current < $start + $pageLength) {
330
                $objects[] = DataObject::get_by_id($row['Source'], $row['ID']);
331
            }
332
        }
333
334
        if (isset($objects)) {
335
            $results = new ArrayList($objects);
336
        } else {
337
            $results = new ArrayList();
338
        }
339
        $list = new PaginatedList($results);
340
        $list->setPageStart($start);
341
        $list->setPageLength($pageLength);
342
        $list->setTotalItems($current+1);
343
        return $list;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $list; (PaginatedList) is incompatible with the return type declared by the abstract method SilverStripe\ORM\Connect\SS_Database::searchEngine of type SilverStripe\ORM\Connect\PaginatedList.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

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

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
344
    }
345
346
    /**
347
     * Allow auto-increment primary key editing on the given table.
348
     * Some databases need to enable this specially.
349
     *
350
     * @param string $table The name of the table to have PK editing allowed on
351
     * @param bool $allow True to start, false to finish
352
     */
353
    public function allowPrimaryKeyEditing($table, $allow = true)
354
    {
355
        $this->query("SET IDENTITY_INSERT \"$table\" " . ($allow ? "ON" : "OFF"));
356
    }
357
358
    /**
359
     * Returns a SQL fragment for querying a fulltext search index
360
     *
361
     * @param string $tableName specific - table name
362
     * @param string $keywords The search query
363
     * @param array $fields The list of field names to search on, or null to include all
364
     * @return string Clause, or null if keyword set is empty or the string with JOIN clause to be added to SQL query
365
     */
366
    public function fullTextSearchMSSQL($tableName, $keywords, $fields = null)
367
    {
368
        // Make sure we are getting an array of fields
369
        if (isset($fields) && !is_array($fields)) {
370
            $fields = array($fields);
371
        }
372
373
        // Strip unfriendly characters, SQLServer "CONTAINS" predicate will crash on & and | and ignore others anyway.
374
        if (function_exists('mb_ereg_replace')) {
375
            $keywords = mb_ereg_replace('[^\w\s]', '', trim($keywords));
376
        } else {
377
            $keywords = $this->escapeString(str_replace(array('&', '|', '!', '"', '\''), '', trim($keywords)));
378
        }
379
380
        // Remove stopwords, concat with ANDs
381
        $keywordList = explode(' ', $keywords);
382
        $keywordList = $this->removeStopwords($keywordList);
383
384
        // remove any empty values from the array
385
        $keywordList = array_filter($keywordList);
386
        if (empty($keywordList)) {
387
            return null;
388
        }
389
390
        $keywords = implode(' AND ', $keywordList);
391
        if ($fields) {
392
            $fieldNames = '"' . implode('", "', $fields) . '"';
393
        } else {
394
            $fieldNames = "*";
395
        }
396
397
        return "CONTAINSTABLE(\"$tableName\", ($fieldNames), '$keywords')";
398
    }
399
400
    /**
401
     * Remove stopwords that would kill a MSSQL full-text query
402
     *
403
     * @param array $keywords
404
     *
405
     * @return array $keywords with stopwords removed
406
     */
407
    public function removeStopwords($keywords)
408
    {
409
        $goodKeywords = array();
410
        foreach ($keywords as $keyword) {
411
            if (in_array($keyword, self::$noiseWords)) {
412
                continue;
413
            }
414
            $goodKeywords[] = trim($keyword);
415
        }
416
        return $goodKeywords;
417
    }
418
419
    /**
420
     * Does this database support transactions?
421
     */
422
    public function supportsTransactions()
423
    {
424
        return $this->supportsTransactions;
425
    }
426
427
    /**
428
     * This is a quick lookup to discover if the database supports particular extensions
429
     * Currently, MSSQL supports no extensions
430
     *
431
     * @param array $extensions List of extensions to check for support of. The key of this array
432
     * will be an extension name, and the value the configuration for that extension. This
433
     * could be one of partitions, tablespaces, or clustering
434
     * @return boolean Flag indicating support for all of the above
435
     */
436
    public function supportsExtensions($extensions = array('partitions', 'tablespaces', 'clustering'))
437
    {
438
        if (isset($extensions['partitions'])) {
439
            return false;
440
        } elseif (isset($extensions['tablespaces'])) {
441
            return false;
442
        } elseif (isset($extensions['clustering'])) {
443
            return false;
444
        } else {
445
            return false;
446
        }
447
    }
448
449
    /**
450
     * Start transaction. READ ONLY not supported.
451
     *
452
     * @param bool $transactionMode
453
     * @param bool $sessionCharacteristics
454
     */
455
    public function transactionStart($transactionMode = false, $sessionCharacteristics = false)
456
    {
457
        if ($this->connector instanceof SQLServerConnector) {
458
            $this->connector->transactionStart();
459
        } else {
460
            $this->query('BEGIN TRANSACTION');
461
        }
462
    }
463
464
    public function transactionSavepoint($savepoint)
465
    {
466
        $this->query("SAVE TRANSACTION \"$savepoint\"");
467
    }
468
469
    public function transactionRollback($savepoint = false)
470
    {
471
        if ($savepoint) {
472
            $this->query("ROLLBACK TRANSACTION \"$savepoint\"");
473
        } elseif ($this->connector instanceof SQLServerConnector) {
474
            $this->connector->transactionRollback();
475
        } else {
476
            $this->query('ROLLBACK TRANSACTION');
477
        }
478
    }
479
480
    public function transactionEnd($chain = false)
481
    {
482
        if ($this->connector instanceof SQLServerConnector) {
483
            $this->connector->transactionEnd();
484
        } else {
485
            $this->query('COMMIT TRANSACTION');
486
        }
487
    }
488
489
    public function comparisonClause($field, $value, $exact = false, $negate = false, $caseSensitive = null, $parameterised = false)
490
    {
491
        if ($exact) {
492
            $comp = ($negate) ? '!=' : '=';
493
        } else {
494
            $comp = 'LIKE';
495
            if ($negate) {
496
                $comp = 'NOT ' . $comp;
497
            }
498
        }
499
500
        // Field definitions are case insensitive by default,
501
        // change used collation for case sensitive searches.
502
        $collateClause = '';
503
        if ($caseSensitive === true) {
504
            if (self::get_collation()) {
505
                $collation = preg_replace('/_CI_/', '_CS_', self::get_collation());
506
            } else {
507
                $collation = 'Latin1_General_CS_AS';
508
            }
509
            $collateClause = ' COLLATE ' . $collation;
510
        } elseif ($caseSensitive === false) {
511
            if (self::get_collation()) {
512
                $collation = preg_replace('/_CS_/', '_CI_', self::get_collation());
513
            } else {
514
                $collation = 'Latin1_General_CI_AS';
515
            }
516
            $collateClause = ' COLLATE ' . $collation;
517
        }
518
519
        $clause = sprintf("%s %s %s", $field, $comp, $parameterised ? '?' : "'$value'");
520
        if ($collateClause) {
521
            $clause .= $collateClause;
522
        }
523
524
        return $clause;
525
    }
526
527
    /**
528
     * Function to return an SQL datetime expression for MSSQL
529
     * used for querying a datetime in a certain format
530
     *
531
     * @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"'
532
     * @param string $format to be used, supported specifiers:
533
     * %Y = Year (four digits)
534
     * %m = Month (01..12)
535
     * %d = Day (01..31)
536
     * %H = Hour (00..23)
537
     * %i = Minutes (00..59)
538
     * %s = Seconds (00..59)
539
     * %U = unix timestamp, can only be used on it's own
540
     * @return string SQL datetime expression to query for a formatted datetime
541
     */
542
    public function formattedDatetimeClause($date, $format)
543
    {
544
        preg_match_all('/%(.)/', $format, $matches);
545
        foreach ($matches[1] as $match) {
546
            if (array_search($match, array('Y', 'm', 'd', 'H', 'i', 's', 'U')) === false) {
547
                user_error('formattedDatetimeClause(): unsupported format character %' . $match, E_USER_WARNING);
548
            }
549
        }
550
551 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...
552
            $date = "CURRENT_TIMESTAMP";
553
        } elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date)) {
554
            $date = "'$date.000'";
555
        }
556
557
        if ($format == '%U') {
558
            return "DATEDIFF(s, '1970-01-01 00:00:00', DATEADD(hour, DATEDIFF(hour, GETDATE(), GETUTCDATE()), $date))";
559
        }
560
561
        $trans = array(
562
            'Y' => 'yy',
563
            'm' => 'mm',
564
            'd' => 'dd',
565
            'H' => 'hh',
566
            'i' => 'mi',
567
            's' => 'ss',
568
        );
569
570
        $strings = array();
571
        $buffer = $format;
572
        while (strlen($buffer)) {
573
            if (substr($buffer, 0, 1) == '%') {
574
                $f = substr($buffer, 1, 1);
575
                $flen = $f == 'Y' ? 4 : 2;
576
                $strings[] = "RIGHT('0' + CAST(DATEPART({$trans[$f]},$date) AS VARCHAR), $flen)";
577
                $buffer = substr($buffer, 2);
578
            } else {
579
                $pos = strpos($buffer, '%');
580
                if ($pos === false) {
581
                    $strings[] = $buffer;
582
                    $buffer = '';
583
                } else {
584
                    $strings[] = "'".substr($buffer, 0, $pos)."'";
585
                    $buffer = substr($buffer, $pos);
586
                }
587
            }
588
        }
589
590
        return '(' . implode(' + ', $strings) . ')';
591
    }
592
593
    /**
594
     * Function to return an SQL datetime expression for MSSQL.
595
     * used for querying a datetime addition
596
     *
597
     * @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...
598
     * @param string $interval to be added, use the format [sign][integer] [qualifier], e.g. -1 Day, +15 minutes, +1 YEAR
599
     * supported qualifiers:
600
     * - years
601
     * - months
602
     * - days
603
     * - hours
604
     * - minutes
605
     * - seconds
606
     * This includes the singular forms as well
607
     * @return string SQL datetime expression to query for a datetime (YYYY-MM-DD hh:mm:ss) which is the result of the addition
608
     */
609
    public function datetimeIntervalClause($date, $interval)
610
    {
611
        $trans = array(
612
            'year' => 'yy',
613
            'month' => 'mm',
614
            'day' => 'dd',
615
            'hour' => 'hh',
616
            'minute' => 'mi',
617
            'second' => 'ss',
618
        );
619
620
        $singularinterval = preg_replace('/(year|month|day|hour|minute|second)s/i', '$1', $interval);
621
622
        if (
623
            !($params = preg_match('/([-+]\d+) (\w+)/i', $singularinterval, $matches)) ||
624
            !isset($trans[strtolower($matches[2])])
625
        ) {
626
            user_error('datetimeIntervalClause(): invalid interval ' . $interval, E_USER_WARNING);
627
        }
628
629 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...
630
            $date = "CURRENT_TIMESTAMP";
631
        } elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date)) {
632
            $date = "'$date'";
633
        }
634
635
        return "CONVERT(VARCHAR, DATEADD(" . $trans[strtolower($matches[2])] . ", " . (int)$matches[1] . ", $date), 120)";
636
    }
637
638
    /**
639
     * Function to return an SQL datetime expression for MSSQL.
640
     * used for querying a datetime substraction
641
     *
642
     * @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...
643
     * @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"'
644
     * @return string SQL datetime expression to query for the interval between $date1 and $date2 in seconds which is the result of the substraction
645
     */
646
    public function datetimeDifferenceClause($date1, $date2)
647
    {
648 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...
649
            $date1 = "CURRENT_TIMESTAMP";
650
        } elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date1)) {
651
            $date1 = "'$date1'";
652
        }
653
654 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...
655
            $date2 = "CURRENT_TIMESTAMP";
656
        } elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date2)) {
657
            $date2 = "'$date2'";
658
        }
659
660
        return "DATEDIFF(s, $date2, $date1)";
661
    }
662
}
663