Issues (37)

Security Analysis    no request data  

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/MSSQLSchemaManager.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace SilverStripe\MSSQL;
4
5
use Exception;
6
use SilverStripe\ORM\Connect\DBSchemaManager;
7
8
/**
9
 * Represents and handles all schema management for a MSSQL database
10
 */
11
class MSSQLSchemaManager extends DBSchemaManager
12
{
13
14
    /**
15
     * Stores per-request cached constraint checks that come from the database.
16
     *
17
     * @var array
18
     */
19
    protected static $cached_checks = [];
20
21
    /**
22
     * Builds the internal MS SQL Server index name given the silverstripe table and index name
23
     *
24
     * @param string $tableName
25
     * @param string $indexName
26
     * @param string $prefix The optional prefix for the index. Defaults to "ix" for indexes.
27
     * @return string The name of the index
28
     */
29
    public function buildMSSQLIndexName($tableName, $indexName, $prefix = 'ix')
30
    {
31
        $tableName = str_replace('\\', '_', $tableName);
32
        $indexName = str_replace('\\', '_', $indexName);
33
34
        return "{$prefix}_{$tableName}_{$indexName}";
35
    }
36
37
38
    /**
39
     * This will set up the full text search capabilities.
40
     *
41
     * @param string $name Name of full text catalog to use
42
     */
43
    public function createFullTextCatalog($name = 'ftCatalog')
44
    {
45
        $result = $this->fullTextCatalogExists();
46
        if (!$result) {
47
            $this->query("CREATE FULLTEXT CATALOG \"$name\" AS DEFAULT;");
48
        }
49
    }
50
51
    /**
52
     * Check that a fulltext catalog has been created yet.
53
     *
54
     * @param string $name Name of full text catalog to use
55
     * @return boolean
56
     */
57
    public function fullTextCatalogExists($name = 'ftCatalog')
58
    {
59
        return (bool) $this->preparedQuery(
60
            "SELECT name FROM sys.fulltext_catalogs WHERE name = ?;",
61
            array($name)
62
        )->value();
63
    }
64
65
    /**
66
     * Sleep until the catalog has been fully rebuilt. This is a busy wait designed for situations
67
     * when you need to be sure the index is up to date - for example in unit tests.
68
     *
69
     * TODO: move this to Database class? Can we assume this will be useful for all databases?
70
     * Also see the wrapper functions "waitUntilIndexingFinished" in SearchFormTest and TranslatableSearchFormTest
71
     *
72
     * @param int $maxWaitingTime Time in seconds to wait for the database.
73
     */
74
    public function waitUntilIndexingFinished($maxWaitingTime = 15)
75
    {
76
        if (!$this->database->fullTextEnabled()) {
77
            return;
78
        }
79
80
        $this->query("EXEC sp_fulltext_catalog 'ftCatalog', 'Rebuild';");
81
82
        // Busy wait until it's done updating, but no longer than 15 seconds.
83
        $start = time();
84
        while (time() - $start < $maxWaitingTime) {
85
            $status = $this->query("EXEC sp_help_fulltext_catalogs 'ftCatalog';")->first();
86
87
            if (isset($status['STATUS']) && $status['STATUS'] == 0) {
88
                // Idle!
89
                break;
90
            }
91
            sleep(1);
92
        }
93
    }
94
95
    /**
96
     * Check if a fulltext index exists on a particular table name.
97
     *
98
     * @param string $tableName
99
     * @return boolean TRUE index exists | FALSE index does not exist | NULL no support
100
     */
101
    public function fulltextIndexExists($tableName)
102
    {
103
        // Special case for no full text index support
104
        if (!$this->database->fullTextEnabled()) {
105
            return null;
106
        }
107
108
        return (bool) $this->preparedQuery("
109
            SELECT 1 FROM sys.fulltext_indexes i
110
            JOIN sys.objects o ON i.object_id = o.object_id
111
            WHERE o.name = ?",
112
            array($tableName)
113
        )->value();
114
    }
115
116
    /**
117
     * MSSQL stores the primary key column with an internal identifier,
118
     * so a lookup needs to be done to determine it.
119
     *
120
     * @param string $tableName Name of table with primary key column "ID"
121
     * @return string Internal identifier for primary key
122
     */
123
    public function getPrimaryKey($tableName)
124
    {
125
        $indexes = $this->query("EXEC sp_helpindex '$tableName';");
126
        $indexName = '';
127
        foreach ($indexes as $index) {
128
            if ($index['index_keys'] == 'ID') {
129
                $indexName = $index['index_name'];
130
                break;
131
            }
132
        }
133
134
        return $indexName;
135
    }
136
137
    public function createDatabase($name)
138
    {
139
        $this->query("CREATE DATABASE \"$name\"");
140
    }
141
142
    public function dropDatabase($name)
143
    {
144
        $this->query("DROP DATABASE \"$name\"");
145
    }
146
147
    public function databaseExists($name)
148
    {
149
        $databases = $this->databaseList();
150
151
        foreach ($databases as $dbname) {
152
            if ($dbname == $name) {
153
                return true;
154
            }
155
        }
156
        return false;
157
    }
158
159
    public function databaseList()
160
    {
161
        return $this->query('SELECT NAME FROM sys.sysdatabases')->column();
162
    }
163
164
    /**
165
     * Create a new table.
166
     *
167
     * @param string $tableName The name of the table
168
     * @param array $fields A map of field names to field types
169
     * @param array $indexes A map of indexes
170
     * @param array $options An map of additional options.  The available keys are as follows:
171
     *   - 'MSSQLDatabase'/'MySQLDatabase'/'PostgreSQLDatabase' - database-specific options such as "engine" for MySQL.
172
     *   - 'temporary' - If true, then a temporary table will be created
173
     * @param array $advancedOptions
174
     * @return string The table name generated.  This may be different from the table name, for example with temporary tables.
175
     */
176
    public function createTable($tableName, $fields = null, $indexes = null, $options = null, $advancedOptions = null)
177
    {
178
        $fieldSchemas = $indexSchemas = "";
179
        if ($fields) {
180
            foreach ($fields as $k => $v) {
181
                $fieldSchemas .= "\"$k\" $v,\n";
182
            }
183
        }
184
185
        // Temporary tables start with "#" in MSSQL-land
186
        if (!empty($options['temporary'])) {
187
            $tableName = "#$tableName" . '-' . rand(1000000, 9999999);
188
        }
189
190
        try {
191
            $this->query("CREATE TABLE \"$tableName\" (
192
                $fieldSchemas
193
                primary key (\"ID\")
194
            );");
195
        } catch (Exception $e) {
196
            $this->alterationMessage('Could not CREATE TABLE '. $tableName .': '. $e->getMessage(), 'error');
197
        }
198
199
        // we need to generate indexes like this: CREATE INDEX IX_vault_to_export ON vault (to_export);
200
        // This needs to be done AFTER the table creation, so we can set up the fulltext indexes correctly
201
        if ($indexes) {
202
            foreach ($indexes as $k => $v) {
203
                $indexSchemas .= $this->getIndexSqlDefinition($tableName, $k, $v) . "\n";
204
            }
205
        }
206
207
        if ($indexSchemas) {
208
            $this->query($indexSchemas);
209
        }
210
211
        return $tableName;
212
    }
213
214
    /**
215
     * Alter a table's schema.
216
     * @param string $tableName The name of the table to alter
217
     * @param array $newFields New fields, a map of field name => field schema
218
     * @param array $newIndexes New indexes, a map of index name => index type
219
     * @param array $alteredFields Updated fields, a map of field name => field schema
220
     * @param array $alteredIndexes Updated indexes, a map of index name => index type
221
     * @param array $alteredOptions
222
     * @param array $advancedOptions
223
     */
224
    public function alterTable($tableName, $newFields = null, $newIndexes = null, $alteredFields = null, $alteredIndexes = null, $alteredOptions=null, $advancedOptions=null)
225
    {
226
        $alterList = array();
227
228
        // drop any fulltext indexes that exist on the table before altering the structure
229
        if ($this->fulltextIndexExists($tableName)) {
230
            $alterList[] = "\nDROP FULLTEXT INDEX ON \"$tableName\";";
231
        }
232
233
        if ($newFields) {
234
            foreach ($newFields as $k => $v) {
235
                $alterList[] = "ALTER TABLE \"$tableName\" ADD \"$k\" $v";
236
            }
237
        }
238
239
        if ($alteredFields) {
240
            foreach ($alteredFields as $k => $v) {
241
                $alterList[] = $this->alterTableAlterColumn($tableName, $k, $v);
242
            }
243
        }
244
245
        if ($alteredIndexes) {
246
            foreach ($alteredIndexes as $k => $v) {
247
                $alterList[] = $this->getIndexSqlDefinition($tableName, $k, $v);
248
            }
249
        }
250
251
        if ($newIndexes) {
252
            foreach ($newIndexes as $k => $v) {
253
                $alterList[] = $this->getIndexSqlDefinition($tableName, $k, $v);
254
            }
255
        }
256
257
        if ($alterList) {
258
            foreach ($alterList as $alteration) {
259
                if ($alteration != '') {
260
                    $this->query($alteration);
261
                }
262
            }
263
        }
264
    }
265
266
    /**
267
     * Given the table and column name, retrieve the constraint name for that column
268
     * in the table.
269
     *
270
     * @param string $tableName Table name column resides in
271
     * @param string $columnName Column name the constraint is for
272
     * @return string|null
273
     */
274
    public function getConstraintName($tableName, $columnName)
275
    {
276
        return $this->preparedQuery("
277
            SELECT CONSTRAINT_NAME
278
            FROM INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE
279
            WHERE TABLE_NAME = ? AND COLUMN_NAME = ?",
280
            array($tableName, $columnName)
281
        )->value();
282
    }
283
284
    /**
285
     * Given a table and column name, return a check constraint clause for that column in
286
     * the table.
287
     *
288
     * This is an expensive query, so it is cached per-request and stored by table. The initial
289
     * call for a table that has not been cached will query all columns and store that
290
     * so subsequent calls are fast.
291
     *
292
     * @param string $tableName Table name column resides in
293
     * @param string $columnName Column name the constraint is for
294
     * @return string The check string
295
     */
296
    public function getConstraintCheckClause($tableName, $columnName)
297
    {
298
        // Check already processed table columns
299
        if (isset(self::$cached_checks[$tableName])) {
300
            if (!isset(self::$cached_checks[$tableName][$columnName])) {
301
                return null;
302
            }
303
            return self::$cached_checks[$tableName][$columnName];
304
        }
305
306
        // Regenerate cehcks for this table
307
        $checks = array();
308
        foreach ($this->preparedQuery("
309
            SELECT CAST(CHECK_CLAUSE AS TEXT) AS CHECK_CLAUSE, COLUMN_NAME
310
            FROM INFORMATION_SCHEMA.CHECK_CONSTRAINTS AS CC
311
            INNER JOIN INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE AS CCU ON CCU.CONSTRAINT_NAME = CC.CONSTRAINT_NAME
312
            WHERE TABLE_NAME = ?",
313
            array($tableName)
314
        ) as $record) {
315
            $checks[$record['COLUMN_NAME']] = $record['CHECK_CLAUSE'];
316
        }
317
        self::$cached_checks[$tableName] = $checks;
318
319
        // Return via cached records
320
        return $this->getConstraintCheckClause($tableName, $columnName);
321
    }
322
323
    /**
324
     * Return the name of the default constraint applied to $tableName.$colName.
325
     * Will return null if no such constraint exists
326
     *
327
     * @param string $tableName Name of the table
328
     * @param string $colName Name of the column
329
     * @return string|null
330
     */
331
    protected function defaultConstraintName($tableName, $colName)
332
    {
333
        return $this->preparedQuery("
334
            SELECT s.name --default name
335
            FROM sys.sysobjects s
336
            join sys.syscolumns c ON s.parent_obj = c.id
337
            WHERE s.xtype = 'd'
338
            and c.cdefault = s.id
339
            and parent_obj = OBJECT_ID(?)
340
            and c.name = ?",
341
            array($tableName, $colName)
342
        )->value();
343
    }
344
345
    /**
346
     * Get enum values from a constraint check clause.
347
     *
348
     * @param string $clause Check clause to parse values from
349
     * @return array Enum values
350
     */
351
    protected function enumValuesFromCheckClause($clause)
352
    {
353
        $segments = preg_split('/ +OR *\[/i', $clause);
354
        $constraints = array();
355
        foreach ($segments as $segment) {
356
            $bits = preg_split('/ *= */', $segment);
357
            for ($i = 1; $i < sizeof($bits); $i += 2) {
358
                array_unshift($constraints, substr(rtrim($bits[$i], ')'), 1, -1));
359
            }
360
        }
361
        return $constraints;
362
    }
363
364
    /*
365
     * Creates an ALTER expression for a column in MS SQL
366
     *
367
     * @param string $tableName Name of the table to be altered
368
     * @param string $colName   Name of the column to be altered
369
     * @param string $colSpec   String which contains conditions for a column
370
     * @return string
371
     */
372
    protected function alterTableAlterColumn($tableName, $colName, $colSpec)
373
    {
374
375
        // First, we split the column specifications into parts
376
        // TODO: this returns an empty array for the following string: int(11) not null auto_increment
377
        //		 on second thoughts, why is an auto_increment field being passed through?
378
        $pattern = '/^(?<definition>[\w()]+)\s?(?<null>(?:not\s)?null)?\s?(?<default>default\s[\w\']+)?\s?(?<check>check\s?[\w()\'",\s]+)?$/i';
379
        $matches = array();
380
        preg_match($pattern, $colSpec, $matches);
381
382
        // drop the index if it exists
383
        $alterQueries = array();
384
385
        // drop *ALL* indexes on a table before proceeding
386
        // this won't drop primary keys, though
387
        $indexes = $this->indexNames($tableName);
388
        $indexes = array_filter($indexes);
389
390
        foreach ($indexes as $indexName) {
391
            $alterQueries[] = "IF EXISTS (SELECT name FROM sys.indexes WHERE name = '$indexName' AND object_id = object_id(SCHEMA_NAME() + '.$tableName')) DROP INDEX \"$indexName\" ON \"$tableName\";";
392
        }
393
394
        $prefix = "ALTER TABLE \"$tableName\" ";
395
396
        // Remove the old default prior to adjusting the column.
397
        if ($defaultConstraintName = $this->defaultConstraintName($tableName, $colName)) {
398
            $alterQueries[] = "$prefix DROP CONSTRAINT \"$defaultConstraintName\";";
399
        }
400
401
        if (isset($matches['definition'])) {
402
            // We will prevent any changes being made to the ID column.
403
            // Primary key indexes will have a fit if we do anything here.
404
            if ($colName != 'ID') {
405
406
                // SET null / not null
407
                $nullFragment = empty($matches['null']) ? '' : " {$matches['null']}";
408
                $alterQueries[] = "$prefix ALTER COLUMN \"$colName\" {$matches['definition']}$nullFragment;";
409
410
                // Add a default back
411
                if (!empty($matches['default'])) {
412
                    $alterQueries[] = "$prefix ADD {$matches['default']} FOR \"$colName\";";
413
                }
414
415
                // SET check constraint (The constraint HAS to be dropped)
416
                if (!empty($matches['check'])) {
417
                    $constraint = $this->getConstraintName($tableName, $colName);
418
                    if ($constraint) {
419
                        $alterQueries[] = "$prefix DROP CONSTRAINT {$constraint};";
420
                    }
421
422
                    //NOTE: 'with nocheck' seems to solve a few problems I've been having for modifying existing tables.
423
                    $alterQueries[] = "$prefix WITH NOCHECK ADD CONSTRAINT \"{$tableName}_{$colName}_check\" {$matches['check']};";
424
                }
425
            }
426
        }
427
428
        return implode("\n", $alterQueries);
429
    }
430
431
    /**
432
     * @param string $oldTableName
433
     * @param string $newTableName
434
     */
435
    public function renameTable($oldTableName, $newTableName)
436
    {
437
        $this->query("EXEC sp_rename \"$oldTableName\", \"$newTableName\"");
438
    }
439
440
    /**
441
     * Checks a table's integrity and repairs it if necessary.
442
     * NOTE: MSSQL does not appear to support any vacuum or optimise commands
443
     *
444
     * @var string $tableName The name of the table.
445
     * @return boolean Return true if the table has integrity after the method is complete.
446
     */
447
    public function checkAndRepairTable($tableName)
448
    {
449
        return true;
450
    }
451
452
    /**
453
     * @param string $tableName
454
     * @param string $fieldName
455
     * @param string $fieldSpec
456
     */
457
    public function createField($tableName, $fieldName, $fieldSpec)
458
    {
459
        $this->query("ALTER TABLE \"$tableName\" ADD \"$fieldName\" $fieldSpec");
460
    }
461
462
    /**
463
     * Change the database type of the given field.
464
     *
465
     * @param string $tableName The name of the tbale the field is in.
466
     * @param string $fieldName The name of the field to change.
467
     * @param string $fieldSpec The new field specification
468
     */
469
    public function alterField($tableName, $fieldName, $fieldSpec)
470
    {
471
        $this->query("ALTER TABLE \"$tableName\" CHANGE \"$fieldName\" \"$fieldName\" $fieldSpec");
472
    }
473
474
    /**
475
     * @param string $tableName
476
     * @param string $oldName
477
     * @param string $newName
478
     *
479
     */
480
    public function renameField($tableName, $oldName, $newName)
481
    {
482
        $this->query("EXEC sp_rename @objname = '$tableName.$oldName', @newname = '$newName', @objtype = 'COLUMN'");
483
    }
484
485
    /**
486
     * @param string $table
487
     *
488
     * @return array
489
     */
490
    public function fieldList($table)
491
    {
492
        $fieldRecords = $this->preparedQuery("SELECT ordinal_position, column_name, data_type, column_default,
493
            is_nullable, character_maximum_length, numeric_precision, numeric_scale, collation_name
494
            FROM information_schema.columns WHERE table_name = ?
495
            ORDER BY ordinal_position;",
496
            [$table]
497
        );
498
499
        // Cache the records from the query - otherwise a lack of multiple active result sets
500
        // will cause subsequent queries to fail in this method
501
        $fields = [];
502
        $output = [];
503
504
        foreach ($fieldRecords as $record) {
505
            if ($record) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $record of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
506
                $fields[] = $record;
507
            }
508
        }
509
510
        foreach ($fields as $field) {
511
            switch ($field['data_type']) {
512
                case 'int':
513
                case 'bigint':
514
                case 'numeric':
515
                case 'float':
516
                case 'bit':
517
                    if ($field['data_type'] != 'bigint' && $field['data_type'] != 'int' && $sizeSuffix = $field['numeric_precision']) {
518
                        $field['data_type'] .= "($sizeSuffix)";
519
                    }
520 View Code Duplication
                    if ($field['is_nullable'] == 'YES') {
521
                        $field['data_type'] .= ' null';
522
                    } else {
523
                        $field['data_type'] .= ' not null';
524
                    }
525
526 View Code Duplication
                    if ($field['column_default']) {
527
                        $default = substr($field['column_default'], 2, -2);
528
                        $field['data_type'] .= " default $default";
529
                    }
530
531
                    break;
532
533
                case 'decimal':
534
                    if ($field['numeric_precision']) {
535
                        $sizeSuffix = $field['numeric_precision'] . ',' . $field['numeric_scale'];
536
                        $field['data_type'] .= "($sizeSuffix)";
537
                    }
538
539 View Code Duplication
                    if ($field['is_nullable'] == 'YES') {
540
                        $field['data_type'] .= ' null';
541
                    } else {
542
                        $field['data_type'] .= ' not null';
543
                    }
544
545 View Code Duplication
                    if ($field['column_default']) {
546
                        $default = substr($field['column_default'], 2, -2);
547
                        $field['data_type'] .= " default $default";
548
                    }
549
550
                    break;
551
552
                case 'nvarchar':
553
                case 'varchar':
554
                    //Check to see if there's a constraint attached to this column:
555
                    $clause = $this->getConstraintCheckClause($table, $field['column_name']);
556
557
                    if ($clause) {
558
                        $constraints = $this->enumValuesFromCheckClause($clause);
559
                        $default = substr($field['column_default'], 2, -2);
560
561
                        $field['data_type'] = $this->enum(array(
562
                            'default' => $default,
563
                            'name' => $field['column_name'],
564
                            'enums' => $constraints,
565
                            'table' => $table
566
                        ));
567
568
                        break;
569
                    }
570
571
                default:
572
                    $sizeSuffix = $field['character_maximum_length'];
573
574
                    if ($sizeSuffix == '-1') {
575
                        $sizeSuffix = 'max';
576
                    }
577
578
                    if ($sizeSuffix) {
579
                        $field['data_type'] .= "($sizeSuffix)";
580
                    }
581
582 View Code Duplication
                    if ($field['is_nullable'] == 'YES') {
583
                        $field['data_type'] .= ' null';
584
                    } else {
585
                        $field['data_type'] .= ' not null';
586
                    }
587
588 View Code Duplication
                    if (isset($field['column_default']) && $field['column_default']) {
0 ignored issues
show
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...
589
                        $default = substr($field['column_default'], 2, -2);
590
591
                        $field['data_type'] .= " default '$default'";
592
                    }
593
            }
594
595
            $output[$field['column_name']] = $field;
596
        }
597
598
        return $output;
599
    }
600
601
    /**
602
     * Create an index on a table.
603
     * @param string $tableName The name of the table.
604
     * @param string $indexName The name of the index.
605
     * @param string $indexSpec The specification of the index, see SS_Database::requireIndex() for more details.
606
     */
607
    public function createIndex($tableName, $indexName, $indexSpec)
608
    {
609
        $this->query($this->getIndexSqlDefinition($tableName, $indexName, $indexSpec));
610
    }
611
612
613
    /**
614
     * This takes the index spec which has been provided by a class (ie static $indexes = blah blah)
615
     * and turns it into a proper string.
616
     * Some indexes may be arrays, such as fulltext and unique indexes, and this allows database-specific
617
     * arrays to be created. See {@link requireTable()} for details on the index format.
618
     *
619
     * @see http://dev.mysql.com/doc/refman/5.0/en/create-index.html
620
     * @see parseIndexSpec() for approximate inverse
621
     *
622
     * @param string|array $indexSpec
623
     * @return string
624
     */
625
    protected function convertIndexSpec($indexSpec)
626
    {
627
        $indexSpec['type'] = trim($indexSpec['type']);
628
        $spec = parent::convertIndexSpec($indexSpec);
629
630
        return $spec;
631
    }
632
633
    /**
634
     * Return SQL for dropping and recreating an index
635
     *
636
     * @param string $tableName Name of table to create this index against
637
     * @param string $indexName Name of this index
638
     * @param array|string $indexSpec Index specification, either as a raw string
639
     * or parsed array form
640
     * @return string The SQL required to generate this index
641
     */
642
    protected function getIndexSqlDefinition($tableName, $indexName, $indexSpec)
643
    {
644
        if (is_array($indexSpec['columns'])) {
645
            $indexSpec['columns'] = $this->implodeColumnList($indexSpec['columns']);
646
        }
647
648
        $index = $this->buildMSSQLIndexName($tableName, $indexName);
649
650
        $drop = "IF EXISTS (SELECT name FROM sys.indexes WHERE name = '$index' AND object_id = object_id(SCHEMA_NAME() + '.$tableName')) DROP INDEX $index ON \"$tableName\";";
651
652
        // create a type-specific index
653
        if ($indexSpec['type'] == 'fulltext') {
654
            if(!$this->database->fullTextEnabled()) {
655
                return '';
656
            }
657
            // enable fulltext on this table
658
            $this->createFullTextCatalog();
659
            $primary_key = $this->getPrimaryKey($tableName);
660
661
            if ($primary_key) {
662
                return "$drop CREATE FULLTEXT INDEX ON \"$tableName\" ({$indexSpec['columns']})"
663
                     . "KEY INDEX $primary_key WITH CHANGE_TRACKING AUTO;";
664
            }
665
        }
666
667
        if ($indexSpec['type'] == 'unique') {
668
            return "$drop CREATE UNIQUE INDEX $index ON \"$tableName\" ({$indexSpec['columns']});";
669
        }
670
671
        return "$drop CREATE INDEX $index ON \"$tableName\" ({$indexSpec['columns']});";
672
    }
673
674
    public function alterIndex($tableName, $indexName, $indexSpec)
675
    {
676
        $this->createIndex($tableName, $indexName, $indexSpec);
677
    }
678
679
    /**
680
     * Return the list of indexes in a table.
681
     *
682
     * @param string $table The table name.
683
     * @return array
684
     */
685
    public function indexList($table)
686
    {
687
        $indexes = $this->query("EXEC sp_helpindex '$table';");
688
        $indexList = [];
689
690
        // Enumerate all basic indexes
691
        foreach ($indexes as $index) {
692
            if (strpos($index['index_description'], 'unique') !== false) {
693
                $indexType = 'unique ';
694
            } else {
695
                $indexType = 'index ';
696
            }
697
698
            // Extract name from index
699
            $baseIndexName = $index['index_name'];
700
            $indexName = substr($index['index_name'], strlen($baseIndexName));
701
702
            $indexList[$baseIndexName] = [
703
                'name' => $indexName,
704
                'columns' => $this->explodeColumnString($index['index_keys']),
705
                'type' => $indexType
706
            ];
707
        }
708
709
        // Now we need to check to see if we have any fulltext indexes attached to this table:
710
        if ($this->database->fullTextEnabled()) {
711
            $result = $this->query('EXEC sp_help_fulltext_columns;');
712
713
            // Extract columns from this fulltext definition
714
            $columns = [];
715
            foreach ($result as $row) {
716
                if ($row['TABLE_NAME'] == $table) {
717
                    $columns[] = $row['FULLTEXT_COLUMN_NAME'];
718
                }
719
            }
720
721
            if (!empty($columns)) {
722
                $indexList['SearchFields'] = [
723
                    'name' => 'SearchFields',
724
                    'columns' => $columns,
725
                    'type' => 'fulltext'
726
                ];
727
            }
728
        }
729
730
        return $indexList;
731
    }
732
733
    /**
734
     * For a given table name, get all the internal index names,
735
     * except for those that are primary keys and fulltext indexes.
736
     *
737
     * @param string $tableName
738
     * @return array
739
     */
740
    public function indexNames($tableName)
741
    {
742
        return $this->preparedQuery('
743
            SELECT ind.name FROM sys.indexes ind
744
            INNER JOIN sys.tables t ON ind.object_id = t.object_id
745
            WHERE is_primary_key = 0 AND t.name = ? AND ind.name IS NOT NULL',
746
            [$tableName]
747
        )->column();
748
    }
749
750
    public function tableList()
751
    {
752
        $tables = [];
753
754
        foreach ($this->query("EXEC sp_tables @table_owner = 'dbo';") as $record) {
755
            $tables[strtolower($record['TABLE_NAME'])] = $record['TABLE_NAME'];
756
        }
757
758
        return $tables;
759
    }
760
761
    /**
762
     * Return a boolean type-formatted string
763
     * We use 'bit' so that we can do numeric-based comparisons
764
     *
765
     * @param array $values Contains a tokenised list of info about this data type
766
     * @return string
767
     */
768
    public function boolean($values)
769
    {
770
        $default = ($values['default']) ? '1' : '0';
771
        return 'bit not null default ' . $default;
772
    }
773
774
    /**
775
     * Return a date type-formatted string.
776
     *
777
     * @param array $values Contains a tokenised list of info about this data type
778
     * @return string
779
     */
780
    public function date($values)
781
    {
782
        return 'date null';
783
    }
784
785
    /**
786
     * Return a decimal type-formatted string
787
     *
788
     * @param array $values Contains a tokenised list of info about this data type
789
     * @return string
790
     */
791
    public function decimal($values)
792
    {
793
        // Avoid empty strings being put in the db
794
        if ($values['precision'] == '') {
795
            $precision = 1;
796
        } else {
797
            $precision = $values['precision'];
798
        }
799
800
        $defaultValue = '0';
801
802
        if (isset($values['default']) && is_numeric($values['default'])) {
803
            $defaultValue = $values['default'];
804
        }
805
806
        return "decimal($precision) not null default $defaultValue";
807
    }
808
809
    /**
810
     * Return a enum type-formatted string
811
     *
812
     * @param array $values Contains a tokenised list of info about this data type
813
     * @return string
814
     */
815
    public function enum($values)
816
    {
817
        // Enums are a bit different. We'll be creating a varchar(255) with a constraint of all the
818
        // usual enum options.
819
        // NOTE: In this one instance, we are including the table name in the values array
820
821
        $maxLength = max(array_map('strlen', $values['enums']));
822
823
        return "varchar($maxLength) not null default '" . $values['default']
824
            . "' check(\"" . $values['name'] . "\" in ('" . implode("','", $values['enums'])
825
            . "'))";
826
    }
827
828
    /**
829
     * @todo Make this work like {@link MySQLDatabase::set()}
830
     *
831
     * @param array $values
832
     * @return string
833
     */
834
    public function set($values)
835
    {
836
        return $this->enum($values);
837
    }
838
839
    /**
840
     * Return a float type-formatted string.
841
     *
842
     * @param array $values Contains a tokenised list of info about this data type
843
     * @return string
844
     */
845
    public function float($values)
846
    {
847
        return 'float(53) not null default ' . $values['default'];
848
    }
849
850
    /**
851
     * Return a int type-formatted string
852
     *
853
     * @param array $values Contains a tokenised list of info about this data type
854
     * @return string
855
     */
856
    public function int($values)
857
    {
858
        return 'int not null default ' . (int) $values['default'];
859
    }
860
861
    /**
862
     * Return a bigint type-formatted string
863
     *
864
     * @param array $values Contains a tokenised list of info about this data type
865
     * @return string
866
     */
867
    public function bigint($values)
868
    {
869
        return 'bigint not null default ' . (int) $values['default'];
870
    }
871
872
    /**
873
     * Return a datetime type-formatted string
874
     * For MS SQL, we simply return the word 'timestamp', no other parameters are necessary
875
     *
876
     * @param array $values Contains a tokenised list of info about this data type
877
     * @return string
878
     */
879
    public function datetime($values)
880
    {
881
        return 'datetime null';
882
    }
883
884
    /**
885
     * Return a text type-formatted string
886
     *
887
     * @param array $values Contains a tokenised list of info about this data type
888
     * @return string
889
     */
890
    public function text($values)
891
    {
892
        $collation = MSSQLDatabase::get_collation();
893
        $collationSQL = $collation ? " COLLATE $collation" : "";
894
        return "nvarchar(max)$collationSQL null";
895
    }
896
897
    /**
898
     * Return a time type-formatted string.
899
     *
900
     * @param array $values Contains a tokenised list of info about this data type
901
     * @return string
902
     */
903
    public function time($values)
904
    {
905
        return 'time null';
906
    }
907
908
    /**
909
     * Return a varchar type-formatted string
910
     *
911
     * @param array $values Contains a tokenised list of info about this data type
912
     * @return string
913
     */
914
    public function varchar($values)
915
    {
916
        $collation = MSSQLDatabase::get_collation();
917
        $collationSQL = $collation ? " COLLATE $collation" : "";
918
        return "nvarchar(" . $values['precision'] . ")$collationSQL null";
919
    }
920
921
    /**
922
     * Return a 4 digit numeric type.
923
     *
924
     * @param array $values
925
     * @return string
926
     */
927
    public function year($values)
928
    {
929
        return 'numeric(4)';
930
    }
931
932
    /**
933
     * This returns the column which is the primary key for each table
934
     *
935
     * @param bool $asDbValue
936
     * @param bool $hasAutoIncPK
937
     * @return string
938
     */
939
    public function IdColumn($asDbValue = false, $hasAutoIncPK = true)
940
    {
941
        if ($asDbValue) {
942
            return 'int not null';
943
        } elseif ($hasAutoIncPK) {
944
            return 'int identity(1,1)';
945
        } else {
946
            return 'int not null';
947
        }
948
    }
949
950
    public function hasTable($tableName)
951
    {
952
        $sqlTable = str_replace('_', '\\_', $this->database->quoteString($tableName));
953
        $has = $this->query("SELECT COUNT(*) FROM information_schema.tables WHERE table_name = $sqlTable")->value();
954
955
        return ($has && $has > 0);
956
    }
957
958
    /**
959
     * Returns the values of the given enum field
960
     * NOTE: Experimental; introduced for db-abstraction and may changed before 2.4 is released.
961
     *
962
     * @param string $tableName
963
     * @param string $fieldName
964
     * @return array
965
     */
966
    public function enumValuesForField($tableName, $fieldName)
967
    {
968
        $classes = [];
969
970
        // Get the enum of all page types from the SiteTree table
971
        $clause = $this->getConstraintCheckClause($tableName, $fieldName);
972
        if ($clause) {
973
            $classes = $this->enumValuesFromCheckClause($clause);
974
        }
975
976
        return $classes;
977
    }
978
979
    /**
980
     * @param string $type
981
     * @return string
982
     */
983
    public function dbDataType($type)
984
    {
985
        $values = array(
986
            'unsigned integer'=>'BIGINT'
987
        );
988
989
        if (isset($values[$type])) {
990
            return $values[$type];
991
        } else {
992
            return '';
993
        }
994
    }
995
996
    /**
997
     * @param string $tableName
998
     * @param string $indexName
999
     * @param mixed $spec
1000
     */
1001
    protected function indexKey($tableName, $indexName, $spec)
1002
    {
1003
        return $this->buildMSSQLIndexName($tableName, $indexName);
1004
    }
1005
}
1006