mirror of
https://github.com/silverstripe/silverstripe-mssql
synced 2024-10-22 08:05:53 +02:00
Compare commits
140 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
34fb10580a | ||
|
9d76e2a042 | ||
|
8b519f9bcf | ||
|
579050c8f7 | ||
|
7be531dd7f | ||
|
321b9fe890 | ||
|
23d4614204 | ||
|
6b3959ba48 | ||
|
406fcee3cd | ||
|
76fb2b29dc | ||
|
2b6a70e529 | ||
|
aa21b10005 | ||
|
ada270c884 | ||
|
0f8c146e99 | ||
|
dac4be1a51 | ||
|
761b42f24a | ||
|
d9ec128735 | ||
|
6a25ac4ab4 | ||
|
a34d67c753 | ||
|
bdc3197351 | ||
|
da6cefac06 | ||
|
551ccfd352 | ||
|
97d8750f3d | ||
|
9309cf3a50 | ||
|
f3574a616b | ||
|
2e64e44087 | ||
|
a3931654b7 | ||
|
04cf84be71 | ||
|
95a28e3ffe | ||
|
1d5d71c956 | ||
|
23e867a918 | ||
|
31eaa833ed | ||
|
4badb07597 | ||
|
8da02fba62 | ||
|
2eaa607a67 | ||
|
2cdd30f7d3 | ||
|
1fab860174 | ||
|
8101ca851d | ||
|
4357cd38c3 | ||
|
22b367be66 | ||
|
1ec1ca86aa | ||
|
ade1ea69d7 | ||
|
be2e9469fd | ||
|
dd23af4278 | ||
|
1fedcfb37e | ||
|
16513227cf | ||
|
b7eb89cd17 | ||
|
15ff8640a2 | ||
|
05faf24483 | ||
|
093df443ea | ||
|
a6a9b25901 | ||
|
73976c111a | ||
|
d50ba80512 | ||
|
0bf7212b6e | ||
|
758fe810a5 | ||
|
a9b1e03202 | ||
|
752664613e | ||
|
e884a1cc31 | ||
|
e3db6f916d | ||
|
fc58ae6571 | ||
|
988d36d0d3 | ||
|
91a2a6ceb4 | ||
|
696c889fc1 | ||
|
01d376f501 | ||
|
1560bad60c | ||
|
7119d5ac80 | ||
|
13cdb07539 | ||
|
9ae0cecc36 | ||
|
55c56b9b66 | ||
|
fc3df54eab | ||
|
e500b024e3 | ||
|
bd18c0a1f3 | ||
|
d6a075f9c7 | ||
|
91548f76be | ||
|
7c369a383a | ||
|
baf3d39cc0 | ||
|
e527665c5c | ||
|
f64c662124 | ||
|
f34996ae1c | ||
|
d17ae37411 | ||
|
334c2634ac | ||
|
37b4005052 | ||
|
93eab9c132 | ||
|
4de7829e4c | ||
|
08eea5cde3 | ||
|
a74470ff52 | ||
|
357482d6c9 | ||
|
530c97f671 | ||
|
e752144a6e | ||
|
05e706ea18 | ||
|
22d53b036b | ||
|
0465f1b1a2 | ||
|
a4c269bf62 | ||
|
49625242c1 | ||
|
84ec3a2014 | ||
|
f1daa29592 | ||
|
05d24b0843 | ||
|
30f017335b | ||
|
731874772c | ||
|
a872e30256 | ||
|
cb01716d39 | ||
|
c7ae517c99 | ||
|
b6082a023e | ||
|
375d440677 | ||
|
4ae94d286f | ||
|
cdf1c475a5 | ||
|
e4349f83a6 | ||
|
2312b49b9c | ||
|
a9444a8313 | ||
|
6cbe886a33 | ||
|
c1ba0fafc9 | ||
|
4b933d7e73 | ||
|
00b81bbdd4 | ||
|
b772956ce8 | ||
|
6f5de764d2 | ||
|
e15e444acc | ||
|
718de0b232 | ||
|
c8d1fbe03f | ||
|
ddd0794720 | ||
|
3542d25c4c | ||
|
7ea6652e56 | ||
|
c104222019 | ||
|
bff9718dd6 | ||
|
8f3b255c5e | ||
|
9e1cdc8cfa | ||
|
7b6899b3f0 | ||
|
9a86a6d9a5 | ||
|
058c29bf69 | ||
|
e83c14236c | ||
|
247470ccc8 | ||
|
ecea643f76 | ||
|
14cf1d243c | ||
|
ae1494c313 | ||
|
35d04ac387 | ||
|
d2082107b5 | ||
|
e8ba221024 | ||
|
159dc13659 | ||
|
ba4f843819 | ||
|
083517d02b | ||
|
75b1933ca3 |
44
.appveyor.yml
Normal file
44
.appveyor.yml
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
build: false
|
||||||
|
shallow_clone: false
|
||||||
|
platform: 'x86'
|
||||||
|
clone_folder: C:\projects\silverstripe
|
||||||
|
|
||||||
|
services:
|
||||||
|
- mssql2014
|
||||||
|
|
||||||
|
before_test:
|
||||||
|
- sqlcmd -S "(local)\SQL2014" -Q "Use [master]; CREATE DATABASE [appveyor]"
|
||||||
|
- SET PATH=C:\Program Files\OpenSSL;C:\tools\php;%PATH%
|
||||||
|
- cinst -y php -version 5.6.11
|
||||||
|
- cd c:\tools\php
|
||||||
|
- ps: cat php.ini-production | %{$_ -replace "memory_limit = 128M","memory_limit = 256M"} | Out-File -Encoding "Default" php.ini
|
||||||
|
- echo date.timezone="UTC" >> php.ini
|
||||||
|
- echo extension_dir=ext >> php.ini
|
||||||
|
- echo extension=php_openssl.dll >> php.ini
|
||||||
|
- echo extension=php_mbstring.dll >> php.ini
|
||||||
|
- echo extension=php_curl.dll >> php.ini
|
||||||
|
- echo extension=php_gd2.dll >> php.ini
|
||||||
|
- echo extension=php_tidy.dll >> php.ini
|
||||||
|
- echo extension=php_fileinfo.dll >> php.ini
|
||||||
|
- php -r "readfile('http://getcomposer.org/installer');" | php
|
||||||
|
- php -r "readfile('https://dl.dropboxusercontent.com/u/7129062/sqlsrv_unofficial_3.0.2.2.zip');" > sqlsrv.zip
|
||||||
|
- unzip sqlsrv.zip
|
||||||
|
- copy sqlsrv_unofficial_3.0.2.2\x64\*.dll ext
|
||||||
|
- echo extension=php_sqlsrv_56_nts.dll >> php.ini
|
||||||
|
- echo extension=php_pdo_sqlsrv_56_nts.dll >> php.ini
|
||||||
|
- ps: echo "php c:\tools\php\composer.phar %*" | Out-File -Encoding "Default" composer.bat
|
||||||
|
- type composer.bat
|
||||||
|
- composer --version
|
||||||
|
- git clone --branch=windows-support git://github.com/silverstripe-labs/silverstripe-travis-support.git c:\projects\travis-support
|
||||||
|
- php c:\projects\travis-support\travis_setup.php --source c:\projects\silverstripe --target c:\projects\assembled
|
||||||
|
- cd c:\projects\assembled
|
||||||
|
|
||||||
|
test_script:
|
||||||
|
- cd c:\projects\assembled
|
||||||
|
- rename phpunit.xml.dist phpunit.xml.dist.orig
|
||||||
|
- ps: cat phpunit.xml.dist.orig | %{$_ -replace "colors=`"true`"","colors=`"false`""} | Out-File -Encoding "Default" phpunit.xml.dist
|
||||||
|
- php .\vendor\phpunit\phpunit\composer\bin\phpunit framework/tests
|
||||||
|
|
||||||
|
environment:
|
||||||
|
DB: MSSQL
|
||||||
|
CORE_RELEASE: master
|
17
.editorconfig
Normal file
17
.editorconfig
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# For more information about the properties used in this file,
|
||||||
|
# please see the EditorConfig documentation:
|
||||||
|
# http://editorconfig.org
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[{*.yml,package.json}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
# The indent size used in the package.json file cannot be changed:
|
||||||
|
# https://github.com/npm/npm/pull/3180#issuecomment-16336516
|
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/tests export-ignore
|
||||||
|
/.scrutinizer.yml export-ignore
|
5
.scrutinizer.yml
Normal file
5
.scrutinizer.yml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
inherit: true
|
||||||
|
checks:
|
||||||
|
php:
|
||||||
|
code_rating: true
|
||||||
|
duplication: true
|
8
.upgrade.yml
Normal file
8
.upgrade.yml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
mappings:
|
||||||
|
MSSQLAzureDatabase: SilverStripe\MSSQL\MSSQLAzureDatabase
|
||||||
|
MSSQLDatabase: SilverStripe\MSSQL\MSSQLDatabase
|
||||||
|
MSSQLDatabaseConfigurationHelper: SilverStripe\MSSQL\MSSQLDatabaseConfigurationHelper
|
||||||
|
MSSQLQueryBuilder: SilverStripe\MSSQL\MSSQLQueryBuilder
|
||||||
|
MSSQLSchemaManager: SilverStripe\MSSQL\MSSQLSchemaManager
|
||||||
|
SQLServerConnector: SilverStripe\MSSQL\SQLServerConnector
|
||||||
|
SQLServerQuery: SilverStripe\MSSQL\SQLServerQuery
|
29
LICENSE
Normal file
29
LICENSE
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
BSD 3-Clause License
|
||||||
|
|
||||||
|
Copyright (c) 2018, SilverStripe Ltd.
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
* Neither the name of the copyright holder nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
68
README.md
68
README.md
@ -1,14 +1,78 @@
|
|||||||
# SQL Server Database Module
|
# SQL Server Database Module
|
||||||
|
|
||||||
|
Allows SilverStripe to use SQL Server databases.
|
||||||
|
|
||||||
|
[![Build status](https://ci.appveyor.com/api/projects/status/hep0l5kbhu64n7l3/branch/master?svg=true)](https://ci.appveyor.com/project/sminnee/silverstripe-mssql-nwvfq/branch/master)
|
||||||
|
|
||||||
## Maintainer Contact
|
## Maintainer Contact
|
||||||
|
|
||||||
* Sean Harvey (Nickname: halkyon)
|
* Sean Harvey (Nickname: halkyon)
|
||||||
<sean (at) silverstripe (dot) com>
|
<sean (at) silverstripe (dot) com>
|
||||||
|
|
||||||
|
* Damian Mooyman (@tractorcow)
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
Requirements list can be found here: http://doc.silverstripe.org/modules:mssql
|
* SilverStripe 4+
|
||||||
|
* SQL Server 2008, 2008 R2, or 2012.
|
||||||
|
|
||||||
|
`mssql` PHP api is no longer supported as of 2.0
|
||||||
|
|
||||||
|
### *nix
|
||||||
|
|
||||||
|
Linux support is only available via the PDO extension. This requires:
|
||||||
|
|
||||||
|
* [dblib](http://www.php.net/manual/en/ref.pdo-dblib.php)
|
||||||
|
* [FreeTDS](http://freetds.org)
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
On windows you can either connect via PDO or `sqlsrv`. Both options require the
|
||||||
|
[SQL Server Driver for PHP](https://msdn.microsoft.com/library/dn865013.aspx?f=255&MSPPError=-2147217396). "sqlsrv" 3.0+
|
||||||
|
|
||||||
|
Note: [SQL Server Express](http://www.microsoft.com/express/Database/) can also be used which is provided free by Microsoft. However, it has limitations such as 10GB maximum database storage.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Installation instructions can be found here: http://doc.silverstripe.org/modules:mssql
|
These steps will install the latest SilverStripe stable, along with this module using [Composer](http://getcomposer.org/):
|
||||||
|
|
||||||
|
* Install SilverStripe: `composer create-project silverstripe/installer /my/website/folder`
|
||||||
|
* Install module: `cd /my/website/folder && composer require silverstripe/mssql ^2`
|
||||||
|
* Open the SilverStripe installer by browsing to install.php, e.g. **http://localhost/silverstripe/install.php**
|
||||||
|
* Select **SQL Server 2008+** in the database list and enter your SQL Server database details
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
*Q: SQL Server resides on a remote host (a different machine) and I can't connect to it from mine.*
|
||||||
|
|
||||||
|
A: Please ensure you have enabled TCP access using **SQL Server Configuration Manager** and [opened firewall ports](http://msdn.microsoft.com/en-us/library/ms175043.aspx).
|
||||||
|
|
||||||
|
*Q: I just installed SQL Server, but it says that it cannot connect*
|
||||||
|
|
||||||
|
A: Sometimes SQL Server will be installed as a non-default instance name, e.g. "SQLExpress" instead of "MSSQLSERVER" (the default.)
|
||||||
|
If this is the case, you'll need to declare the instance name when setting the server in your PHP database configuration. For example: **(local)\SQLExpress**. The first part before the slash indicates the server host, or IP address. In this case, (local) indicates localhost, which is the same server PHP is running on. The second part is the SQL Server instance name to connect to.
|
||||||
|
|
||||||
|
*Q: I'm getting unicode SQL Server errors connecting to SQL Server database (e.g. Unicode data in a Unicode-only collation or ntext data cannot be sent to clients using DB-Library (such as ISQL) or ODBC version 3.7 or earlier)*
|
||||||
|
|
||||||
|
A: If you are using FreeTDS make sure you're using TDS version 8.0 in **freetds.conf**. If on Windows, ensure you use the [SQL Server Driver for PHP](http://www.microsoft.com/downloads/en/details.aspx?displaylang=en&FamilyID=ccdf728b-1ea0-48a8-a84a-5052214caad9) and **NOT** the mssql drivers provided by PHP.
|
||||||
|
|
||||||
|
*Q: Using FreeTDS I can't connect to my SQL Server database. An error in PHP says the server doesn't exist*
|
||||||
|
|
||||||
|
A: Make sure you've got an entry in **/etc/freetds/freetds.conf** that points to your server. For example:
|
||||||
|
|
||||||
|
[myserver]
|
||||||
|
host = myserver.mydomain.com
|
||||||
|
port = 1433
|
||||||
|
tds version = 8.0
|
||||||
|
|
||||||
|
Then you can use "myserver" (the bit in square brackets above) as the server name when connecting to the database.
|
||||||
|
Note that if you're running Macports, the file is located in **/opt/local/etc/freetds/freetds.conf**.
|
||||||
|
|
||||||
|
Alternatively, if you don't want to keep adding more entries to the freetds.conf to nominate more SQL Server locations,
|
||||||
|
you can instead use the full the host/ip and port combination, such as "myserver:1433" (1433 being the default SQL Server port.)
|
||||||
|
and ensure the "tds version = 8.0" is set globally in the freetds.conf file.
|
||||||
|
|
||||||
|
**Note**: Use *tabs* not spaces when editing freetds.conf, otherwise it will not load the configuration you have specified!
|
||||||
|
|
||||||
|
**Note**: Certain distributions of Linux use [SELinux](http://fedoraproject.org/wiki/SELinux) which could block access to your SQL Server database. A rule may need to be added to allow this traffic through.
|
||||||
|
|
||||||
|
32
_config/connectors.yml
Normal file
32
_config/connectors.yml
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
name: mssqlconnectors
|
||||||
|
---
|
||||||
|
SilverStripe\Core\Injector\Injector:
|
||||||
|
# Connect using PDO
|
||||||
|
MSSQLPDODatabase:
|
||||||
|
class: 'SilverStripe\MSSQL\MSSQLDatabase'
|
||||||
|
properties:
|
||||||
|
connector: %$PDOConnector
|
||||||
|
schemaManager: %$MSSQLSchemaManager
|
||||||
|
queryBuilder: %$MSSQLQueryBuilder
|
||||||
|
# Uses sqlsrv_connect
|
||||||
|
MSSQLDatabase:
|
||||||
|
class: 'SilverStripe\MSSQL\MSSQLDatabase'
|
||||||
|
properties:
|
||||||
|
connector: %$SQLServerConnector
|
||||||
|
schemaManager: %$MSSQLSchemaManager
|
||||||
|
queryBuilder: %$MSSQLQueryBuilder
|
||||||
|
# Uses sqlsrv_connect to connect to a MS Azure Database
|
||||||
|
MSSQLAzureDatabase:
|
||||||
|
class: 'SilverStripe\MSSQL\MSSQLAzureDatabase'
|
||||||
|
properties:
|
||||||
|
connector: %$SQLServerConnector
|
||||||
|
schemaManager: %$MSSQLSchemaManager
|
||||||
|
queryBuilder: %$MSSQLQueryBuilder
|
||||||
|
SQLServerConnector:
|
||||||
|
class: 'SilverStripe\MSSQL\SQLServerConnector'
|
||||||
|
type: prototype
|
||||||
|
MSSQLSchemaManager:
|
||||||
|
class: 'SilverStripe\MSSQL\MSSQLSchemaManager'
|
||||||
|
MSSQLQueryBuilder:
|
||||||
|
class: 'SilverStripe\MSSQL\MSSQLQueryBuilder'
|
2
_config/mssql.yml
Normal file
2
_config/mssql.yml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
MSSQLDatabase:
|
||||||
|
collation: null
|
54
_register_database.php
Normal file
54
_register_database.php
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use SilverStripe\Dev\Install\DatabaseAdapterRegistry;
|
||||||
|
use SilverStripe\MSSQL\MSSQLDatabaseConfigurationHelper;
|
||||||
|
|
||||||
|
// PDO connector for MS SQL Server
|
||||||
|
/** @skipUpgrade */
|
||||||
|
DatabaseAdapterRegistry::register(array(
|
||||||
|
'class' => 'MSSQLPDODatabase',
|
||||||
|
'module' => 'mssql',
|
||||||
|
'title' => 'SQL Server 2008 (using PDO)',
|
||||||
|
'helperPath' => __DIR__.'/code/MSSQLDatabaseConfigurationHelper.php',
|
||||||
|
'helperClass' => MSSQLDatabaseConfigurationHelper::class,
|
||||||
|
'supported' => !!MSSQLDatabaseConfigurationHelper::getPDODriver(),
|
||||||
|
'missingExtensionText' =>
|
||||||
|
'Either the <a href="http://www.php.net/manual/en/book.pdo.php">PDO Extension</a> or
|
||||||
|
the <a href="http://www.php.net/manual/en/ref.pdo-sqlsrv.php">SQL Server PDO Driver</a>
|
||||||
|
are unavailable. Please install or enable these and refresh this page.'
|
||||||
|
));
|
||||||
|
|
||||||
|
// Basic driver using sqlsrv connector
|
||||||
|
/** @skipUpgrade */
|
||||||
|
DatabaseAdapterRegistry::register(array(
|
||||||
|
'class' => 'MSSQLDatabase',
|
||||||
|
'module' => 'mssql',
|
||||||
|
'title' => 'SQL Server 2008 (using sqlsrv)',
|
||||||
|
'helperPath' => __DIR__.'/code/MSSQLDatabaseConfigurationHelper.php',
|
||||||
|
'helperClass' => MSSQLDatabaseConfigurationHelper::class,
|
||||||
|
'supported' => function_exists('sqlsrv_connect'),
|
||||||
|
'missingExtensionText' =>
|
||||||
|
'The <a href="http://www.microsoft.com/sqlserver/2005/en/us/PHP-Driver.aspx">sqlsrv</a>
|
||||||
|
PHP extensions is not available. Please install or enable it and refresh this page.',
|
||||||
|
'fields' => array_merge(DatabaseAdapterRegistry::get_default_fields(), array(
|
||||||
|
// @todo - do we care about windows authentication for PDO/SQL Server?
|
||||||
|
'windowsauthentication' => array(
|
||||||
|
'title' => 'Use Windows authentication? (leave blank for false)',
|
||||||
|
'default' => ''
|
||||||
|
)
|
||||||
|
))
|
||||||
|
));
|
||||||
|
|
||||||
|
// MS Azure uses an online database
|
||||||
|
/** @skipUpgrade */
|
||||||
|
DatabaseAdapterRegistry::register(array(
|
||||||
|
'class' => 'MSSQLAzureDatabase',
|
||||||
|
'module' => 'mssql',
|
||||||
|
'title' => 'MS Azure Database (using sqlsrv)',
|
||||||
|
'helperPath' => __DIR__.'/code/MSSQLDatabaseConfigurationHelper.php',
|
||||||
|
'helperClass' => MSSQLDatabaseConfigurationHelper::class,
|
||||||
|
'supported' => function_exists('sqlsrv_connect'),
|
||||||
|
'missingExtensionText' =>
|
||||||
|
'The <a href="http://www.microsoft.com/sqlserver/2005/en/us/PHP-Driver.aspx">sqlsrv</a>
|
||||||
|
PHP extension is not available. Please install or enable it and refresh this page.'
|
||||||
|
));
|
1
code-of-conduct.md
Normal file
1
code-of-conduct.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
When having discussions about this module in issues or pull request please adhere to the [SilverStripe Community Code of Conduct](https://docs.silverstripe.org/en/contributing/code_of_conduct).
|
@ -1,8 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\MSSQL;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Specific support for SQL Azure databases running on Windows Azure.
|
* Specific support for SQL Azure databases running on Windows Azure.
|
||||||
* "sqlsrv" for PHP MUST be installed to use SQL Azure. It does not support
|
* Currently only supports the SQLSRV driver from Microsoft.
|
||||||
* the mssql_*() functions in PHP, as SQL Azure is Windows only.
|
|
||||||
*
|
*
|
||||||
* Some important things about SQL Azure:
|
* Some important things about SQL Azure:
|
||||||
*
|
*
|
||||||
@ -16,35 +18,63 @@
|
|||||||
* Fulltext indexes are not supported.
|
* Fulltext indexes are not supported.
|
||||||
*
|
*
|
||||||
* @author Sean Harvey <sean at silverstripe dot com>
|
* @author Sean Harvey <sean at silverstripe dot com>
|
||||||
* @package mssql
|
|
||||||
*/
|
*/
|
||||||
class MSSQLAzureDatabase extends MSSQLDatabase {
|
class MSSQLAzureDatabase extends MSSQLDatabase
|
||||||
|
{
|
||||||
|
|
||||||
protected $fullTextEnabled = false;
|
/**
|
||||||
|
* List of parameters used to create new Azure connections between databases
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $parameters = array();
|
||||||
|
|
||||||
public function __construct($parameters) {
|
public function fullTextEnabled()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __construct($parameters)
|
||||||
|
{
|
||||||
$this->connectDatabase($parameters);
|
$this->connectDatabase($parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connect to a SQL Azure database with the given parameters.
|
* Connect to a SQL Azure database with the given parameters.
|
||||||
* @param array $parameters Connection parameters set by environment
|
* @param array $parameters Connection parameters set by environment
|
||||||
* @return resource SQL Azure database connection link
|
* - server: The server, eg, localhost
|
||||||
|
* - username: The username to log on with
|
||||||
|
* - password: The password to log on with
|
||||||
|
* - database: The database to connect to
|
||||||
|
* - windowsauthentication: Not supported for Azure
|
||||||
*/
|
*/
|
||||||
protected function connectDatabase($parameters) {
|
public function connect($parameters)
|
||||||
$this->dbConn = sqlsrv_connect($parameters['server'], array(
|
{
|
||||||
'Database' => $parameters['database'],
|
$this->parameters = $parameters;
|
||||||
'UID' => $parameters['username'],
|
$this->connectDatabase($parameters['database']);
|
||||||
'PWD' => $parameters['password'],
|
}
|
||||||
'MultipleActiveResultSets' => '0'
|
|
||||||
));
|
|
||||||
|
|
||||||
$this->tableList = $this->fieldList = $this->indexList = null;
|
/**
|
||||||
$this->database = $parameters['database'];
|
* Connect to a database using the provided parameters
|
||||||
$this->active = true;
|
*
|
||||||
$this->mssql = false; // mssql functions don't work with this database
|
* @param string $database
|
||||||
$this->fullTextEnabled = false;
|
*/
|
||||||
|
protected function connectDatabase($database)
|
||||||
|
{
|
||||||
|
$parameters = $this->parameters;
|
||||||
|
$parameters['database'] = $database;
|
||||||
|
$parameters['multipleactiveresultsets'] = 0;
|
||||||
|
|
||||||
|
// Ensure that driver is available (required by PDO)
|
||||||
|
if (empty($parameters['driver'])) {
|
||||||
|
$parameters['driver'] = $this->getDatabaseServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify connector of parameters, instructing the connector
|
||||||
|
// to connect immediately to the Azure database
|
||||||
|
$this->connector->connect($parameters, true);
|
||||||
|
|
||||||
|
// Configure the connection
|
||||||
$this->query('SET QUOTED_IDENTIFIER ON');
|
$this->query('SET QUOTED_IDENTIFIER ON');
|
||||||
$this->query('SET TEXTSIZE 2147483647');
|
$this->query('SET TEXTSIZE 2147483647');
|
||||||
}
|
}
|
||||||
@ -52,20 +82,32 @@ class MSSQLAzureDatabase extends MSSQLDatabase {
|
|||||||
/**
|
/**
|
||||||
* Switches to the given database.
|
* Switches to the given database.
|
||||||
*
|
*
|
||||||
* If the database doesn't exist, you should call
|
|
||||||
* createDatabase() after calling selectDatabase()
|
|
||||||
*
|
|
||||||
* IMPORTANT: SQL Azure doesn't support "USE", so we need
|
* IMPORTANT: SQL Azure doesn't support "USE", so we need
|
||||||
* to reinitialize the database connection with the requested
|
* to reinitialize the database connection with the requested
|
||||||
* database name.
|
* database name.
|
||||||
|
* @see http://msdn.microsoft.com/en-us/library/windowsazure/ee336288.aspx
|
||||||
*
|
*
|
||||||
* @param string $dbname The database name to switch to
|
* @param string $name The database name to switch to
|
||||||
|
* @param bool $create
|
||||||
|
* @param bool|int $errorLevel
|
||||||
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function selectDatabase($dbname) {
|
public function selectDatabase($name, $create = false, $errorLevel = E_USER_ERROR)
|
||||||
global $databaseConfig;
|
{
|
||||||
$parameters = $databaseConfig;
|
$this->fullTextEnabled = null;
|
||||||
$parameters['database'] = $dbname;
|
if (!$this->schemaManager->databaseExists($name)) {
|
||||||
$this->connectDatabase($parameters);
|
// Check DB creation permisson
|
||||||
|
if (!$create) {
|
||||||
|
if ($errorLevel !== false) {
|
||||||
|
user_error("Attempted to connect to non-existing database \"$name\"", $errorLevel);
|
||||||
|
}
|
||||||
|
// Unselect database
|
||||||
|
$this->connector->unloadDatabase();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$this->schemaManager->createDatabase($name);
|
||||||
|
}
|
||||||
|
$this->connectDatabase($name);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -1,4 +1,18 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\MSSQL;
|
||||||
|
|
||||||
|
use SilverStripe\Core\Config\Configurable;
|
||||||
|
use SilverStripe\Core\Injector\Injectable;
|
||||||
|
use SilverStripe\Core\ClassInfo;
|
||||||
|
use SilverStripe\ORM\ArrayList;
|
||||||
|
use SilverStripe\ORM\Connect\Database;
|
||||||
|
use SilverStripe\ORM\DataList;
|
||||||
|
use SilverStripe\ORM\DB;
|
||||||
|
use SilverStripe\ORM\DataObject;
|
||||||
|
use SilverStripe\ORM\PaginatedList;
|
||||||
|
use SilverStripe\ORM\Queries\SQLSelect;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Microsoft SQL Server 2008+ connector class.
|
* Microsoft SQL Server 2008+ connector class.
|
||||||
*
|
*
|
||||||
@ -35,46 +49,16 @@
|
|||||||
*
|
*
|
||||||
* References:
|
* References:
|
||||||
* @see http://freetds.org
|
* @see http://freetds.org
|
||||||
*
|
|
||||||
* @package mssql
|
|
||||||
*/
|
*/
|
||||||
class MSSQLDatabase extends SS_Database {
|
class MSSQLDatabase extends Database
|
||||||
|
{
|
||||||
/**
|
use Configurable;
|
||||||
* Connection to the DBMS.
|
use Injectable;
|
||||||
* @var resource
|
|
||||||
*/
|
|
||||||
protected $dbConn;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* True if we are connected to a database.
|
|
||||||
* @var boolean
|
|
||||||
*/
|
|
||||||
protected $active;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The name of the database.
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $database;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If true, use the mssql_... functions.
|
|
||||||
* If false use the sqlsrv_... functions
|
|
||||||
*/
|
|
||||||
protected $mssql = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stores the affected rows of the last query.
|
|
||||||
* Used by sqlsrv functions only, as sqlsrv_rows_affected
|
|
||||||
* accepts a result instead of a database handle.
|
|
||||||
*/
|
|
||||||
protected $lastAffectedRows;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Words that will trigger an error if passed to a SQL Server fulltext search
|
* Words that will trigger an error if passed to a SQL Server fulltext search
|
||||||
*/
|
*/
|
||||||
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");
|
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');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transactions will work with FreeTDS, but not entirely with sqlsrv driver on Windows with MARS enabled.
|
* Transactions will work with FreeTDS, but not entirely with sqlsrv driver on Windows with MARS enabled.
|
||||||
@ -84,7 +68,7 @@ class MSSQLDatabase extends SS_Database {
|
|||||||
* - figure out SAVEPOINTS
|
* - figure out SAVEPOINTS
|
||||||
* - READ ONLY transactions
|
* - READ ONLY transactions
|
||||||
*/
|
*/
|
||||||
protected $supportsTransactions = false;
|
protected $supportsTransactions = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cached flag to determine if full-text is enabled. This is set by
|
* Cached flag to determine if full-text is enabled. This is set by
|
||||||
@ -95,16 +79,31 @@ class MSSQLDatabase extends SS_Database {
|
|||||||
protected $fullTextEnabled = null;
|
protected $fullTextEnabled = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ignore
|
* @var bool
|
||||||
*/
|
*/
|
||||||
protected static $collation = null;
|
protected $transactionNesting = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the default collation of the MSSQL nvarchar fields that we create.
|
* Set the default collation of the MSSQL nvarchar fields that we create.
|
||||||
* We don't apply this to the database as a whole, so that we can use unicode collations.
|
* We don't apply this to the database as a whole, so that we can use unicode collations.
|
||||||
|
*
|
||||||
|
* @param string $collation
|
||||||
*/
|
*/
|
||||||
public static function set_collation($collation) {
|
public static function set_collation($collation)
|
||||||
self::$collation = $collation;
|
{
|
||||||
|
static::config()->set('collation', $collation);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default collation of the MSSQL nvarchar fields that we create.
|
||||||
|
* We don't apply this to the database as a whole, so that we can use
|
||||||
|
* unicode collations.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function get_collation()
|
||||||
|
{
|
||||||
|
return static::config()->get('collation');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -114,65 +113,17 @@ class MSSQLDatabase extends SS_Database {
|
|||||||
* - username: The username to log on with
|
* - username: The username to log on with
|
||||||
* - password: The password to log on with
|
* - password: The password to log on with
|
||||||
* - database: The database to connect to
|
* - database: The database to connect to
|
||||||
|
* - windowsauthentication: Set to true to use windows authentication
|
||||||
|
* instead of username/password
|
||||||
*/
|
*/
|
||||||
public function __construct($parameters) {
|
public function connect($parameters)
|
||||||
if(function_exists('mssql_connect')) {
|
{
|
||||||
$this->mssql = true;
|
parent::connect($parameters);
|
||||||
} else if(function_exists('sqlsrv_connect')) {
|
|
||||||
$this->mssql = false;
|
|
||||||
} else {
|
|
||||||
user_error("Neither the mssql_connect() nor the sqlsrv_connect() functions are available. Please install the PHP native mssql module, or the Microsoft-provided sqlsrv module.", E_USER_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
if($this->mssql) {
|
|
||||||
// Switch to utf8 connection charset
|
|
||||||
ini_set('mssql.charset', 'utf8');
|
|
||||||
$this->dbConn = mssql_connect($parameters['server'], $parameters['username'], $parameters['password'], true);
|
|
||||||
} else {
|
|
||||||
// Disable default warnings as errors behaviour for sqlsrv to keep it in line with mssql functions
|
|
||||||
if(ini_get('sqlsrv.WarningsReturnAsErrors')) {
|
|
||||||
ini_set('sqlsrv.WarningsReturnAsErrors', 'Off');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Windows authentication doesn't require a username and password
|
|
||||||
if(defined('MSSQL_USE_WINDOWS_AUTHENTICATION') && MSSQL_USE_WINDOWS_AUTHENTICATION == true) {
|
|
||||||
$connectionInfo = array(
|
|
||||||
'CharacterSet' => 'UTF-8',
|
|
||||||
'MultipleActiveResultSets' => true
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
$connectionInfo = array(
|
|
||||||
'UID' => $parameters['username'],
|
|
||||||
'PWD' => $parameters['password'],
|
|
||||||
'CharacterSet' => 'UTF-8',
|
|
||||||
'MultipleActiveResultSets' => true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
$this->dbConn = sqlsrv_connect($parameters['server'], $connectionInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!$this->dbConn) {
|
|
||||||
$this->databaseError("Couldn't connect to MS SQL database");
|
|
||||||
|
|
||||||
} else {
|
|
||||||
$this->database = $parameters['database'];
|
|
||||||
$this->selectDatabase($this->database);
|
|
||||||
|
|
||||||
// Configure the connection
|
// Configure the connection
|
||||||
$this->query('SET QUOTED_IDENTIFIER ON');
|
$this->query('SET QUOTED_IDENTIFIER ON');
|
||||||
$this->query('SET TEXTSIZE 2147483647');
|
$this->query('SET TEXTSIZE 2147483647');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public function __destruct() {
|
|
||||||
if(is_resource($this->dbConn)) {
|
|
||||||
if($this->mssql) {
|
|
||||||
mssql_close($this->dbConn);
|
|
||||||
} else {
|
|
||||||
sqlsrv_close($this->dbConn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether the current SQL Server version has full-text
|
* Checks whether the current SQL Server version has full-text
|
||||||
@ -180,1175 +131,274 @@ class MSSQLDatabase extends SS_Database {
|
|||||||
*
|
*
|
||||||
* @return boolean
|
* @return boolean
|
||||||
*/
|
*/
|
||||||
public function fullTextEnabled() {
|
public function fullTextEnabled()
|
||||||
|
{
|
||||||
if ($this->fullTextEnabled === null) {
|
if ($this->fullTextEnabled === null) {
|
||||||
$isInstalled = (boolean) DB::query("SELECT fulltextserviceproperty('isfulltextinstalled')")->value();
|
$this->fullTextEnabled = $this->updateFullTextEnabled();
|
||||||
$enabledForDb = (boolean) DB::query("
|
|
||||||
SELECT is_fulltext_enabled
|
|
||||||
FROM sys.databases
|
|
||||||
WHERE name = '$this->database'
|
|
||||||
")->value();
|
|
||||||
$this->fullTextEnabled = (boolean) ($isInstalled && $enabledForDb);
|
|
||||||
}
|
}
|
||||||
return $this->fullTextEnabled;
|
return $this->fullTextEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Throw a database error
|
* Checks whether the current SQL Server version has full-text
|
||||||
*/
|
* support installed and full-text is enabled for this database.
|
||||||
function databaseError($message, $errorLevel = E_USER_ERROR) {
|
|
||||||
if(!$this->mssql) {
|
|
||||||
$errorMessages = array();
|
|
||||||
$errors = sqlsrv_errors();
|
|
||||||
if ($errors) foreach($errors as $error) {
|
|
||||||
$errorMessages[] = $error['message'];
|
|
||||||
}
|
|
||||||
$message .= ": \n" . implode("; ",$errorMessages);
|
|
||||||
}
|
|
||||||
|
|
||||||
return parent::databaseError($message, $errorLevel);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This will set up the full text search capabilities.
|
|
||||||
*
|
*
|
||||||
* TODO: make this a _config.php setting
|
|
||||||
* TODO: VERY IMPORTANT: move this so it only gets called upon a dev/build action
|
|
||||||
*/
|
|
||||||
function createFullTextCatalog() {
|
|
||||||
if($this->fullTextEnabled()) {
|
|
||||||
$result = $this->query("SELECT name FROM sys.fulltext_catalogs WHERE name = 'ftCatalog';")->value();
|
|
||||||
if(!$result) $this->query("CREATE FULLTEXT CATALOG ftCatalog AS DEFAULT;");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sleep until the catalog has been fully rebuilt. This is a busy wait designed for situations
|
|
||||||
* when you need to be sure the index is up to date - for example in unit tests.
|
|
||||||
*
|
|
||||||
* TODO: move this to Database class? Can we assume this will be useful for all databases?
|
|
||||||
* Also see the wrapper functions "waitUntilIndexingFinished" in SearchFormTest and TranslatableSearchFormTest
|
|
||||||
*
|
|
||||||
* @param int $maxWaitingTime Time in seconds to wait for the database.
|
|
||||||
*/
|
|
||||||
function waitUntilIndexingFinished($maxWaitingTime = 15) {
|
|
||||||
if($this->fullTextEnabled()) {
|
|
||||||
$this->query("EXEC sp_fulltext_catalog 'ftCatalog', 'Rebuild';");
|
|
||||||
|
|
||||||
// Busy wait until it's done updating, but no longer than 15 seconds.
|
|
||||||
$start = time();
|
|
||||||
while(time()-$start<$maxWaitingTime) {
|
|
||||||
$status = $this->query("EXEC sp_help_fulltext_catalogs 'ftCatalog';")->first();
|
|
||||||
|
|
||||||
if (isset($status['STATUS']) && $status['STATUS']==0) {
|
|
||||||
// Idle!
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
sleep(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Not implemented, needed for PDO
|
|
||||||
*/
|
|
||||||
public function getConnect($parameters) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if this database supports collations
|
|
||||||
* @return boolean
|
* @return boolean
|
||||||
*/
|
*/
|
||||||
public function supportsCollations() {
|
protected function updateFullTextEnabled()
|
||||||
return true;
|
{
|
||||||
}
|
// Check if installed
|
||||||
|
$isInstalled = $this->query("SELECT fulltextserviceproperty('isfulltextinstalled')")->value();
|
||||||
/**
|
if (!$isInstalled) {
|
||||||
* Get the version of MSSQL.
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getVersion() {
|
|
||||||
return trim($this->query("SELECT CONVERT(char(15), SERVERPROPERTY('ProductVersion'))")->value());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the database server, namely mssql.
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getDatabaseServer() {
|
|
||||||
return "mssql";
|
|
||||||
}
|
|
||||||
|
|
||||||
public function query($sql, $errorLevel = E_USER_ERROR) {
|
|
||||||
if(isset($_REQUEST['previewwrite']) && in_array(strtolower(substr($sql,0,strpos($sql,' '))), array('insert','update','delete','replace'))) {
|
|
||||||
Debug::message("Will execute: $sql");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(isset($_REQUEST['showqueries'])) {
|
|
||||||
$starttime = microtime(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
$error = '';
|
|
||||||
if($this->mssql) {
|
|
||||||
$handle = mssql_query($sql, $this->dbConn);
|
|
||||||
$error = mssql_get_last_message();
|
|
||||||
} else {
|
|
||||||
$handle = sqlsrv_query($this->dbConn, $sql);
|
|
||||||
if($handle) $this->lastAffectedRows = sqlsrv_rows_affected($handle);
|
|
||||||
if(function_exists('sqlsrv_errors')) {
|
|
||||||
$errInfo = sqlsrv_errors();
|
|
||||||
if($errInfo) {
|
|
||||||
foreach($errInfo as $info) {
|
|
||||||
$error .= implode(', ', array($info['SQLSTATE'], $info['code'], $info['message']));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(isset($_REQUEST['showqueries'])) {
|
|
||||||
$endtime = round(microtime(true) - $starttime,4);
|
|
||||||
Debug::message("\n$sql\n{$endtime}ms\n", false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!$handle && $errorLevel) $this->databaseError("Couldn't run query ($error): $sql", $errorLevel);
|
|
||||||
return new MSSQLQuery($this, $handle, $this->mssql);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getGeneratedID($table) {
|
|
||||||
//return $this->query("SELECT @@IDENTITY FROM \"$table\"")->value();
|
|
||||||
return $this->query("SELECT IDENT_CURRENT('$table')")->value();
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This is a handy helper function which will return the primary key for any paricular table
|
|
||||||
* In MSSQL, the primary key is often an internal identifier, NOT the standard name (ie, 'ID'),
|
|
||||||
* so we need to do a lookup for it.
|
|
||||||
*/
|
|
||||||
function getPrimaryKey($tableName){
|
|
||||||
$indexes=DB::query("EXEC sp_helpindex '$tableName';");
|
|
||||||
$primary_key='';
|
|
||||||
foreach($indexes as $this_index){
|
|
||||||
if($this_index['index_keys']=='ID'){
|
|
||||||
$primary_key=$this_index['index_name'];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $primary_key;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* OBSOLETE: Get the ID for the next new record for the table.
|
|
||||||
* @param string $table The name of the table
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function getNextID($table) {
|
|
||||||
user_error('getNextID is OBSOLETE (and will no longer work properly)', E_USER_WARNING);
|
|
||||||
$result = $this->query("SELECT MAX(ID)+1 FROM \"$table\"")->value();
|
|
||||||
return $result ? $result : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isActive() {
|
|
||||||
return $this->active ? true : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create the database that is currently selected.
|
|
||||||
*/
|
|
||||||
public function createDatabase() {
|
|
||||||
$this->query("CREATE DATABASE \"$this->database\"");
|
|
||||||
$this->selectDatabase($this->database);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Drop the database that this object is currently connected to.
|
|
||||||
* Use with caution.
|
|
||||||
*/
|
|
||||||
public function dropDatabase() {
|
|
||||||
$db = $this->database;
|
|
||||||
$this->selectDatabase('master');
|
|
||||||
$this->query("DROP DATABASE \"$db\"");
|
|
||||||
$this->active = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the name of the currently selected database
|
|
||||||
*/
|
|
||||||
public function currentDatabase() {
|
|
||||||
return $this->database;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Switches to the given database.
|
|
||||||
*
|
|
||||||
* If the database doesn't exist, you should call
|
|
||||||
* createDatabase() after calling selectDatabase()
|
|
||||||
*
|
|
||||||
* @param string $dbname The database name to switch to
|
|
||||||
*/
|
|
||||||
public function selectDatabase($dbname) {
|
|
||||||
$this->database = $dbname;
|
|
||||||
|
|
||||||
if($this->databaseExists($this->database)) {
|
|
||||||
if($this->mssql) {
|
|
||||||
if(mssql_select_db($this->database, $this->dbConn)) {
|
|
||||||
$this->active = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$this->query("USE \"$this->database\"");
|
|
||||||
$this->active = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->tableList = $this->fieldList = $this->indexList = $this->fullTextEnabled = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the given database exists.
|
|
||||||
* @param string $name Name of database to check exists
|
|
||||||
* @return boolean
|
|
||||||
*/
|
|
||||||
public function databaseExists($name) {
|
|
||||||
$listDBs = $this->query('SELECT NAME FROM sys.sysdatabases');
|
|
||||||
if($listDBs) {
|
|
||||||
foreach($listDBs as $listedDB) {
|
|
||||||
if($listedDB['NAME'] == $name) return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Check if current database is enabled
|
||||||
* Create a new table.
|
$database = $this->getSelectedDatabase();
|
||||||
* @param $tableName The name of the table
|
$enabledForDb = $this->preparedQuery(
|
||||||
* @param $fields A map of field names to field types
|
"SELECT is_fulltext_enabled FROM sys.databases WHERE name = ?",
|
||||||
* @param $indexes A map of indexes
|
array($database)
|
||||||
* @param $options An map of additional options. The available keys are as follows:
|
)->value();
|
||||||
* - 'MSSQLDatabase'/'MySQLDatabase'/'PostgreSQLDatabase' - database-specific options such as "engine" for MySQL.
|
return $enabledForDb;
|
||||||
* - 'temporary' - If true, then a temporary table will be created
|
|
||||||
* @return The table name generated. This may be different from the table name, for example with temporary tables.
|
|
||||||
*/
|
|
||||||
public function createTable($tableName, $fields = null, $indexes = null, $options = null, $advancedOptions = null) {
|
|
||||||
$fieldSchemas = $indexSchemas = "";
|
|
||||||
if($fields) foreach($fields as $k => $v) $fieldSchemas .= "\"$k\" $v,\n";
|
|
||||||
|
|
||||||
// Temporary tables start with "#" in MSSQL-land
|
|
||||||
if(!empty($options['temporary'])) {
|
|
||||||
// Randomize the temp table name to avoid conflicts in the tempdb table which derived databases share
|
|
||||||
$tableName = "#$tableName" . '-' . rand(1000000, 9999999);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->query("CREATE TABLE \"$tableName\" (
|
public function supportsCollations()
|
||||||
$fieldSchemas
|
{
|
||||||
primary key (\"ID\")
|
|
||||||
);");
|
|
||||||
|
|
||||||
//we need to generate indexes like this: CREATE INDEX IX_vault_to_export ON vault (to_export);
|
|
||||||
//This needs to be done AFTER the table creation, so we can set up the fulltext indexes correctly
|
|
||||||
if($indexes) foreach($indexes as $k => $v) {
|
|
||||||
$indexSchemas .= $this->getIndexSqlDefinition($tableName, $k, $v) . "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
if($indexSchemas) $this->query($indexSchemas);
|
|
||||||
|
|
||||||
return $tableName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Alter a table's schema.
|
|
||||||
* @param $table The name of the table to alter
|
|
||||||
* @param $newFields New fields, a map of field name => field schema
|
|
||||||
* @param $newIndexes New indexes, a map of index name => index type
|
|
||||||
* @param $alteredFields Updated fields, a map of field name => field schema
|
|
||||||
* @param $alteredIndexes Updated indexes, a map of index name => index type
|
|
||||||
*/
|
|
||||||
public function alterTable($tableName, $newFields = null, $newIndexes = null, $alteredFields = null, $alteredIndexes = null, $alteredOptions=null, $advancedOptions=null) {
|
|
||||||
$fieldSchemas = $indexSchemas = "";
|
|
||||||
$alterList = array();
|
|
||||||
$indexList = $this->indexList($tableName);
|
|
||||||
|
|
||||||
if($newFields) foreach($newFields as $k => $v) $alterList[] .= "ALTER TABLE \"$tableName\" ADD \"$k\" $v";
|
|
||||||
|
|
||||||
if($alteredFields) {
|
|
||||||
// fulltext indexes need to be dropped if alterting a table
|
|
||||||
if($this->fulltextIndexExists($tableName) === true) {
|
|
||||||
$alterList[] = "\nDROP FULLTEXT INDEX ON \"$tableName\";";
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach($alteredFields as $k => $v) {
|
|
||||||
$val = $this->alterTableAlterColumn($tableName, $k, $v, $indexList);
|
|
||||||
if($val != '') $alterList[] .= $val;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if($alteredIndexes) foreach($alteredIndexes as $k => $v) $alterList[] .= $this->getIndexSqlDefinition($tableName, $k, $v);
|
|
||||||
if($newIndexes) foreach($newIndexes as $k =>$v) $alterList[] .= $this->getIndexSqlDefinition($tableName, $k, $v);
|
|
||||||
|
|
||||||
if($alterList) {
|
|
||||||
foreach($alterList as $alteration) {
|
|
||||||
if($alteration != '') {
|
|
||||||
$this->query($alteration);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a private MSSQL-only function which returns
|
|
||||||
* specific details about a column's constraints (if any)
|
|
||||||
* @param string $tableName Name of table the column exists in
|
|
||||||
* @param string $columnName Name of column to check for
|
|
||||||
*/
|
|
||||||
protected function ColumnConstraints($tableName, $columnName) {
|
|
||||||
$constraint = $this->query("SELECT CC.CONSTRAINT_NAME, CAST(CHECK_CLAUSE AS TEXT) AS CHECK_CLAUSE, COLUMN_NAME FROM INFORMATION_SCHEMA.CHECK_CONSTRAINTS AS CC INNER JOIN INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE AS CCU ON CCU.CONSTRAINT_NAME=CC.CONSTRAINT_NAME WHERE TABLE_NAME='$tableName' AND COLUMN_NAME='" . $columnName . "';")->first();
|
|
||||||
return $constraint;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the name of the default constraint applied to $tableName.$colName.
|
|
||||||
* Will return null if no such constraint exists
|
|
||||||
*/
|
|
||||||
protected function defaultConstraintName($tableName, $colName) {
|
|
||||||
return $this->query("SELECT s.name --default name
|
|
||||||
FROM sys.sysobjects s
|
|
||||||
join sys.syscolumns c ON s.parent_obj = c.id
|
|
||||||
WHERE s.xtype = 'd'
|
|
||||||
and c.cdefault = s.id
|
|
||||||
and parent_obj= OBJECT_ID('$tableName')
|
|
||||||
and c.name = '$colName'")->value();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the actual enum fields from the constraint value:
|
|
||||||
*/
|
|
||||||
protected function EnumValuesFromConstraint($constraint){
|
|
||||||
$segments=preg_split('/ +OR *\[/i', $constraint);
|
|
||||||
$constraints=Array();
|
|
||||||
foreach($segments as $this_segment){
|
|
||||||
$bits=preg_split('/ *= */', $this_segment);
|
|
||||||
|
|
||||||
for($i=1; $i<sizeof($bits); $i+=2)
|
|
||||||
array_unshift($constraints, substr(rtrim($bits[$i], ')'), 1, -1));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return $constraints;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Creates an ALTER expression for a column in MS SQL
|
|
||||||
*
|
|
||||||
* @param $tableName Name of the table to be altered
|
|
||||||
* @param $colName Name of the column to be altered
|
|
||||||
* @param $colSpec String which contains conditions for a column
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
protected function alterTableAlterColumn($tableName, $colName, $colSpec, $indexList){
|
|
||||||
|
|
||||||
// First, we split the column specifications into parts
|
|
||||||
// TODO: this returns an empty array for the following string: int(11) not null auto_increment
|
|
||||||
// on second thoughts, why is an auto_increment field being passed through?
|
|
||||||
$pattern = '/^([\w()]+)\s?((?:not\s)?null)?\s?(default\s[\w\']+)?\s?(check\s?[\w()\'",\s]+)?$/i';
|
|
||||||
$matches=Array();
|
|
||||||
preg_match($pattern, $colSpec, $matches);
|
|
||||||
|
|
||||||
// drop the index if it exists
|
|
||||||
$alterCol='';
|
|
||||||
$indexName = isset($indexList[$colName]['indexname']) ? $indexList[$colName]['indexname'] : null;
|
|
||||||
if($indexName && $colName != 'ID') {
|
|
||||||
$alterCol = "\nDROP INDEX \"$indexName\" ON \"$tableName\";";
|
|
||||||
}
|
|
||||||
|
|
||||||
$prefix="ALTER TABLE \"" . $tableName . "\" ";
|
|
||||||
|
|
||||||
// Remove the old default prior to adjusting the column.
|
|
||||||
if($defaultConstraintName = $this->defaultConstraintName($tableName, $colName)) {
|
|
||||||
$alterCol .= ";\n$prefix DROP CONSTRAINT \"$defaultConstraintName\"";
|
|
||||||
}
|
|
||||||
|
|
||||||
if(isset($matches[1])) {
|
|
||||||
//We will prevent any changes being made to the ID column. Primary key indexes will have a fit if we do anything here.
|
|
||||||
if($colName!='ID'){
|
|
||||||
$alterCol .= ";\n$prefix ALTER COLUMN \"$colName\" $matches[1]";
|
|
||||||
|
|
||||||
// SET null / not null
|
|
||||||
if(!empty($matches[2])) $alterCol .= ";\n$prefix ALTER COLUMN \"$colName\" $matches[1] $matches[2]";
|
|
||||||
|
|
||||||
// Add a default back
|
|
||||||
if(!empty($matches[3])) $alterCol .= ";\n$prefix ADD $matches[3] FOR \"$colName\"";
|
|
||||||
|
|
||||||
// SET check constraint (The constraint HAS to be dropped)
|
|
||||||
if(!empty($matches[4])) {
|
|
||||||
$constraint=$this->ColumnConstraints($tableName, $colName);
|
|
||||||
if($constraint)
|
|
||||||
$alterCol .= ";\n$prefix DROP CONSTRAINT {$constraint['CONSTRAINT_NAME']}";
|
|
||||||
|
|
||||||
//NOTE: 'with nocheck' seems to solve a few problems I've been having for modifying existing tables.
|
|
||||||
$alterCol .= ";\n$prefix WITH NOCHECK ADD CONSTRAINT \"{$tableName}_{$colName}_check\" $matches[4]";
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return isset($alterCol) ? $alterCol : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function renameTable($oldTableName, $newTableName) {
|
|
||||||
$this->query("EXEC sp_rename \"$oldTableName\", \"$newTableName\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks a table's integrity and repairs it if necessary.
|
|
||||||
* NOTE: MSSQL does not appear to support any vacuum or optimise commands
|
|
||||||
*
|
|
||||||
* @var string $tableName The name of the table.
|
|
||||||
* @return boolean Return true if the table has integrity after the method is complete.
|
|
||||||
*/
|
|
||||||
public function checkAndRepairTable($tableName) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function createField($tableName, $fieldName, $fieldSpec) {
|
public function supportsTimezoneOverride()
|
||||||
$this->query("ALTER TABLE \"$tableName\" ADD \"$fieldName\" $fieldSpec");
|
{
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function getDatabaseServer()
|
||||||
* Change the database type of the given field.
|
{
|
||||||
* @param string $tableName The name of the tbale the field is in.
|
return "sqlsrv";
|
||||||
* @param string $fieldName The name of the field to change.
|
|
||||||
* @param string $fieldSpec The new field specification
|
|
||||||
*/
|
|
||||||
public function alterField($tableName, $fieldName, $fieldSpec) {
|
|
||||||
$this->query("ALTER TABLE \"$tableName\" CHANGE \"$fieldName\" \"$fieldName\" $fieldSpec");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function selectDatabase($name, $create = false, $errorLevel = E_USER_ERROR)
|
||||||
* Change the database column name of the given field.
|
{
|
||||||
*
|
$this->fullTextEnabled = null;
|
||||||
* @param string $tableName The name of the tbale the field is in.
|
|
||||||
* @param string $oldName The name of the field to change.
|
return parent::selectDatabase($name, $create, $errorLevel);
|
||||||
* @param string $newName The new name of the field
|
|
||||||
*/
|
|
||||||
public function renameField($tableName, $oldName, $newName) {
|
|
||||||
$fieldList = $this->fieldList($tableName);
|
|
||||||
if(array_key_exists($oldName, $fieldList)) {
|
|
||||||
$this->query("EXEC sp_rename @objname = '$tableName.$oldName', @newname = '$newName', @objtype = 'COLUMN'");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function fieldList($table) {
|
public function clearTable($table)
|
||||||
//This gets us more information than we need, but I've included it all for the moment....
|
{
|
||||||
$fieldRecords = $this->query("SELECT ordinal_position, column_name, data_type, column_default,
|
|
||||||
is_nullable, character_maximum_length, numeric_precision, numeric_scale, collation_name
|
|
||||||
FROM information_schema.columns WHERE table_name = '$table'
|
|
||||||
ORDER BY ordinal_position;");
|
|
||||||
|
|
||||||
// Cache the records from the query - otherwise a lack of multiple active result sets
|
|
||||||
// will cause subsequent queries to fail in this method
|
|
||||||
$fields = array();
|
|
||||||
$output = array();
|
|
||||||
foreach($fieldRecords as $record) {
|
|
||||||
$fields[] = $record;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach($fields as $field) {
|
|
||||||
// Update the data_type field to be a complete column definition string for use by
|
|
||||||
// SS_Database::requireField()
|
|
||||||
switch($field['data_type']){
|
|
||||||
case 'bigint':
|
|
||||||
case 'numeric':
|
|
||||||
case 'float':
|
|
||||||
case 'bit':
|
|
||||||
if($field['data_type'] != 'bigint' && $sizeSuffix = $field['numeric_precision']) {
|
|
||||||
$field['data_type'] .= "($sizeSuffix)";
|
|
||||||
}
|
|
||||||
|
|
||||||
if($field['is_nullable'] == 'YES') {
|
|
||||||
$field['data_type'] .= ' null';
|
|
||||||
} else {
|
|
||||||
$field['data_type'] .= ' not null';
|
|
||||||
}
|
|
||||||
if($field['column_default']) {
|
|
||||||
$default=substr($field['column_default'], 2, -2);
|
|
||||||
$field['data_type'] .= " default $default";
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'decimal':
|
|
||||||
if($field['numeric_precision']) {
|
|
||||||
$sizeSuffix = $field['numeric_precision'] . ',' . $field['numeric_scale'];
|
|
||||||
$field['data_type'] .= "($sizeSuffix)";
|
|
||||||
}
|
|
||||||
|
|
||||||
if($field['is_nullable'] == 'YES') {
|
|
||||||
$field['data_type'] .= ' null';
|
|
||||||
} else {
|
|
||||||
$field['data_type'] .= ' not null';
|
|
||||||
}
|
|
||||||
if($field['column_default']) {
|
|
||||||
$default=substr($field['column_default'], 2, -2);
|
|
||||||
$field['data_type'] .= " default $default";
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'nvarchar':
|
|
||||||
case 'varchar':
|
|
||||||
//Check to see if there's a constraint attached to this column:
|
|
||||||
$constraint=$this->ColumnConstraints($table, $field['column_name']);
|
|
||||||
if($constraint){
|
|
||||||
$constraints=$this->EnumValuesFromConstraint($constraint['CHECK_CLAUSE']);
|
|
||||||
$default=substr($field['column_default'], 2, -2);
|
|
||||||
$field['data_type']=$this->enum(Array('default'=>$default, 'name'=>$field['column_name'], 'enums'=>$constraints, 'table'=>$table));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
$sizeSuffix = $field['character_maximum_length'];
|
|
||||||
if($sizeSuffix == '-1') $sizeSuffix = 'max';
|
|
||||||
if($sizeSuffix) {
|
|
||||||
$field['data_type'] .= "($sizeSuffix)";
|
|
||||||
}
|
|
||||||
|
|
||||||
if($field['is_nullable'] == 'YES') {
|
|
||||||
$field['data_type'] .= ' null';
|
|
||||||
} else {
|
|
||||||
$field['data_type'] .= ' not null';
|
|
||||||
}
|
|
||||||
if($field['column_default']) {
|
|
||||||
$default=substr($field['column_default'], 2, -2);
|
|
||||||
$field['data_type'] .= " default '$default'";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$output[$field['column_name']]=$field;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return $output;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an index on a table.
|
|
||||||
* @param string $tableName The name of the table.
|
|
||||||
* @param string $indexName The name of the index.
|
|
||||||
* @param string $indexSpec The specification of the index, see SS_Database::requireIndex() for more details.
|
|
||||||
*/
|
|
||||||
public function createIndex($tableName, $indexName, $indexSpec) {
|
|
||||||
$this->query($this->getIndexSqlDefinition($tableName, $indexName, $indexSpec));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This takes the index spec which has been provided by a class (ie static $indexes = blah blah)
|
|
||||||
* and turns it into a proper string.
|
|
||||||
* Some indexes may be arrays, such as fulltext and unique indexes, and this allows database-specific
|
|
||||||
* arrays to be created.
|
|
||||||
*/
|
|
||||||
public function convertIndexSpec($indexSpec){
|
|
||||||
if(is_array($indexSpec)){
|
|
||||||
//Here we create a db-specific version of whatever index we need to create.
|
|
||||||
switch($indexSpec['type']){
|
|
||||||
case 'fulltext':
|
|
||||||
$indexSpec='fulltext (' . str_replace(' ', '', $indexSpec['value']) . ')';
|
|
||||||
break;
|
|
||||||
case 'unique':
|
|
||||||
$indexSpec='unique (' . $indexSpec['value'] . ')';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $indexSpec;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return SQL for dropping and recreating an index
|
|
||||||
*/
|
|
||||||
protected function getIndexSqlDefinition($tableName, $indexName, $indexSpec) {
|
|
||||||
$index = 'ix_' . $tableName . '_' . $indexName;
|
|
||||||
$drop = "IF EXISTS (SELECT name FROM sys.indexes WHERE name = '$index') DROP INDEX $index ON \"" . $tableName . "\";";
|
|
||||||
|
|
||||||
if(!is_array($indexSpec)) {
|
|
||||||
$indexSpec=trim($indexSpec, '()');
|
|
||||||
$bits=explode(',', $indexSpec);
|
|
||||||
$indexes="\"" . implode("\",\"", $bits) . "\"";
|
|
||||||
|
|
||||||
return "$drop CREATE INDEX $index ON \"" . $tableName . "\" (" . $indexes . ");";
|
|
||||||
} else {
|
|
||||||
//create a type-specific index
|
|
||||||
if($indexSpec['type'] == 'fulltext') {
|
|
||||||
if($this->fullTextEnabled()) {
|
|
||||||
//Enable full text search.
|
|
||||||
$this->createFullTextCatalog();
|
|
||||||
$primary_key = $this->getPrimaryKey($tableName);
|
|
||||||
|
|
||||||
$query = '';
|
|
||||||
if($this->fullTextIndexExists($tableName)) {
|
|
||||||
$query .= "\nDROP FULLTEXT INDEX ON \"$tableName\";";
|
|
||||||
}
|
|
||||||
$query .= "CREATE FULLTEXT INDEX ON \"$tableName\" ({$indexSpec['value']}) KEY INDEX $primary_key WITH CHANGE_TRACKING AUTO;";
|
|
||||||
return $query;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if($indexSpec['type'] == 'unique') {
|
|
||||||
if(!is_array($indexSpec['value'])) $columns = preg_split('/ *, */', trim($indexSpec['value']));
|
|
||||||
else $columns = $indexSpec['value'];
|
|
||||||
$SQL_columnList = '"' . implode('", "', $columns) . '"';
|
|
||||||
|
|
||||||
return "$drop CREATE UNIQUE INDEX $index ON \"" . $tableName . "\" ($SQL_columnList);";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDbSqlDefinition($tableName, $indexName, $indexSpec){
|
|
||||||
return $indexName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Alter an index on a table.
|
|
||||||
* @param string $tableName The name of the table.
|
|
||||||
* @param string $indexName The name of the index.
|
|
||||||
* @param string $indexSpec The specification of the index, see SS_Database::requireIndex() for more details.
|
|
||||||
*/
|
|
||||||
public function alterIndex($tableName, $indexName, $indexSpec) {
|
|
||||||
$indexSpec = trim($indexSpec);
|
|
||||||
if($indexSpec[0] != '(') {
|
|
||||||
list($indexType, $indexFields) = explode(' ',$indexSpec,2);
|
|
||||||
} else {
|
|
||||||
$indexFields = $indexSpec;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!$indexType) {
|
|
||||||
$indexType = "index";
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->query("DROP INDEX $indexName ON $tableName;");
|
|
||||||
$this->query("ALTER TABLE \"$tableName\" ADD $indexType \"$indexName\" $indexFields");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the list of indexes in a table.
|
|
||||||
* @param string $table The table name.
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function indexList($table) {
|
|
||||||
$indexes = DB::query("EXEC sp_helpindex '$table';");
|
|
||||||
$prefix = '';
|
|
||||||
$indexList = array();
|
|
||||||
|
|
||||||
foreach($indexes as $index) {
|
|
||||||
if(strpos($index['index_description'], 'unique') !== false) {
|
|
||||||
$prefix='unique ';
|
|
||||||
}
|
|
||||||
|
|
||||||
$key = str_replace(', ', ',', $index['index_keys']);
|
|
||||||
$indexList[$key]['indexname'] = $index['index_name'];
|
|
||||||
$indexList[$key]['spec'] = $prefix . '(' . $key . ')';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now we need to check to see if we have any fulltext indexes attached to this table:
|
|
||||||
if($this->fullTextEnabled()) {
|
|
||||||
$result = DB::query('EXEC sp_help_fulltext_columns;');
|
|
||||||
$columns = '';
|
|
||||||
foreach($result as $row) {
|
|
||||||
if($row['TABLE_NAME'] == $table) {
|
|
||||||
$columns .= $row['FULLTEXT_COLUMN_NAME'] . ',';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if($columns!=''){
|
|
||||||
$columns=trim($columns, ',');
|
|
||||||
$indexList['SearchFields']['indexname'] = 'SearchFields';
|
|
||||||
$indexList['SearchFields']['spec'] = 'fulltext (' . $columns . ')';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $indexList;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a list of all the tables in the database.
|
|
||||||
* Table names will all be in lowercase.
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function tableList() {
|
|
||||||
$tables = array();
|
|
||||||
foreach($this->query("EXEC sp_tables @table_owner = 'dbo';") as $record) {
|
|
||||||
$table = strtolower($record['TABLE_NAME']);
|
|
||||||
$tables[$table] = $table;
|
|
||||||
}
|
|
||||||
return $tables;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Empty the given table of call contentTR
|
|
||||||
*/
|
|
||||||
public function clearTable($table) {
|
|
||||||
$this->query("TRUNCATE TABLE \"$table\"");
|
$this->query("TRUNCATE TABLE \"$table\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the number of rows affected by the previous operation.
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function affectedRows() {
|
|
||||||
if($this->mssql) {
|
|
||||||
return mssql_rows_affected($this->dbConn);
|
|
||||||
} else {
|
|
||||||
return $this->lastAffectedRows;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a boolean type-formatted string
|
|
||||||
* We use 'bit' so that we can do numeric-based comparisons
|
|
||||||
*
|
|
||||||
* @params array $values Contains a tokenised list of info about this data type
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function boolean($values) {
|
|
||||||
//Annoyingly, we need to do a good ol' fashioned switch here:
|
|
||||||
($values['default']) ? $default='1' : $default='0';
|
|
||||||
return 'bit not null default ' . $default;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a date type-formatted string.
|
|
||||||
*
|
|
||||||
* @params array $values Contains a tokenised list of info about this data type
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function date($values) {
|
|
||||||
return 'datetime null';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a decimal type-formatted string
|
|
||||||
*
|
|
||||||
* @params array $values Contains a tokenised list of info about this data type
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function decimal($values) {
|
|
||||||
// Avoid empty strings being put in the db
|
|
||||||
if($values['precision'] == '') {
|
|
||||||
$precision = 1;
|
|
||||||
} else {
|
|
||||||
$precision = $values['precision'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$defaultValue = '0';
|
|
||||||
if(isset($values['default']) && is_numeric($values['default'])) {
|
|
||||||
$defaultValue = $values['default'];
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'decimal(' . $precision . ') not null default ' . $defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a enum type-formatted string
|
|
||||||
*
|
|
||||||
* @params array $values Contains a tokenised list of info about this data type
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function enum($values) {
|
|
||||||
// Enums are a bit different. We'll be creating a varchar(255) with a constraint of all the
|
|
||||||
// usual enum options.
|
|
||||||
// NOTE: In this one instance, we are including the table name in the values array
|
|
||||||
|
|
||||||
$maxLength = max(array_map('strlen', $values['enums']));
|
|
||||||
|
|
||||||
return "varchar($maxLength) not null default '" . $values['default']
|
|
||||||
. "' check(\"" . $values['name'] . "\" in ('" . implode("','", $values['enums'])
|
|
||||||
. "'))";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @todo Make this work like {@link MySQLDatabase::set()}
|
|
||||||
*/
|
|
||||||
public function set($values) {
|
|
||||||
return $this->enum($values);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a float type-formatted string.
|
|
||||||
*
|
|
||||||
* @params array $values Contains a tokenised list of info about this data type
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function float($values) {
|
|
||||||
return 'float not null default ' . $values['default'];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a int type-formatted string
|
|
||||||
*
|
|
||||||
* @params array $values Contains a tokenised list of info about this data type
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function int($values) {
|
|
||||||
//We'll be using an 8 digit precision to keep it in line with the serial8 datatype for ID columns
|
|
||||||
return 'numeric(8) not null default ' . (int) $values['default'];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a datetime type-formatted string
|
|
||||||
* For MS SQL, we simply return the word 'timestamp', no other parameters are necessary
|
|
||||||
*
|
|
||||||
* @params array $values Contains a tokenised list of info about this data type
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function ss_datetime($values) {
|
|
||||||
return 'datetime null';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a text type-formatted string
|
|
||||||
*
|
|
||||||
* @params array $values Contains a tokenised list of info about this data type
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function text($values) {
|
|
||||||
$collation = self::$collation ? " COLLATE " . self::$collation : "";
|
|
||||||
return "nvarchar(max)$collation null";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a time type-formatted string.
|
|
||||||
*
|
|
||||||
* @params array $values Contains a tokenised list of info about this data type
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function time($values){
|
|
||||||
return 'time null';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a varchar type-formatted string
|
|
||||||
*
|
|
||||||
* @params array $values Contains a tokenised list of info about this data type
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function varchar($values) {
|
|
||||||
$collation = self::$collation ? " COLLATE " . self::$collation : "";
|
|
||||||
return "nvarchar(" . $values['precision'] . ")$collation null";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a 4 digit numeric type.
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function year($values) {
|
|
||||||
return 'numeric(4)';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This returns the column which is the primary key for each table
|
|
||||||
* In Postgres, it is a SERIAL8, which is the equivalent of an auto_increment
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
function IdColumn($asDbValue=false, $hasAutoIncPK=true){
|
|
||||||
if($asDbValue)
|
|
||||||
return 'bigint not null';
|
|
||||||
else {
|
|
||||||
if($hasAutoIncPK)
|
|
||||||
return 'bigint identity(1,1)';
|
|
||||||
else return 'bigint not null';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the SQL command to get all the tables in this database
|
|
||||||
*/
|
|
||||||
function allTablesSQL(){
|
|
||||||
return "SELECT \"name\" FROM \"sys\".\"tables\";";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if this table exists
|
|
||||||
* @todo Make a proper implementation
|
|
||||||
*/
|
|
||||||
function hasTable($tableName) {
|
|
||||||
$SQL_tableName = Convert::raw2sql($tableName);
|
|
||||||
$value = DB::query("SELECT table_name FROM information_schema.tables WHERE table_name = '$SQL_tableName'")->value();
|
|
||||||
return (bool)$value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the values of the given enum field
|
|
||||||
* NOTE: Experimental; introduced for db-abstraction and may changed before 2.4 is released.
|
|
||||||
*/
|
|
||||||
public function enumValuesForField($tableName, $fieldName) {
|
|
||||||
// Get the enum of all page types from the SiteTree table
|
|
||||||
|
|
||||||
$constraints=$this->ColumnConstraints($tableName, $fieldName);
|
|
||||||
$classes=Array();
|
|
||||||
if($constraints){
|
|
||||||
$constraints=$this->EnumValuesFromConstraint($constraints['CHECK_CLAUSE']);
|
|
||||||
$classes=$constraints;
|
|
||||||
}
|
|
||||||
return $classes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SQL Server uses CURRENT_TIMESTAMP for the current date/time.
|
* SQL Server uses CURRENT_TIMESTAMP for the current date/time.
|
||||||
*/
|
*/
|
||||||
function now() {
|
public function now()
|
||||||
|
{
|
||||||
return 'CURRENT_TIMESTAMP';
|
return 'CURRENT_TIMESTAMP';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the database-specific version of the random() function
|
* Returns the database-specific version of the random() function
|
||||||
*/
|
*/
|
||||||
function random(){
|
public function random()
|
||||||
|
{
|
||||||
return 'RAND()';
|
return 'RAND()';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a lookup table for data types.
|
|
||||||
*
|
|
||||||
* For instance, MSSQL uses 'BIGINT', while MySQL uses 'UNSIGNED'
|
|
||||||
* and PostgreSQL uses 'INT'.
|
|
||||||
*/
|
|
||||||
function dbDataType($type){
|
|
||||||
$values = array(
|
|
||||||
'unsigned integer'=>'BIGINT'
|
|
||||||
);
|
|
||||||
if(isset($values[$type])) return $values[$type];
|
|
||||||
else return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a SQLQuery object into a SQL statement.
|
|
||||||
*/
|
|
||||||
public function sqlQueryToString(SQLQuery $sqlQuery) {
|
|
||||||
if (!$sqlQuery->from) return '';
|
|
||||||
|
|
||||||
if($sqlQuery->orderby && strtoupper(trim($sqlQuery->orderby)) == 'RAND()') $sqlQuery->orderby = "NEWID()";
|
|
||||||
|
|
||||||
//Get the limit and offset
|
|
||||||
$limit='';
|
|
||||||
$offset='0';
|
|
||||||
if(is_array($sqlQuery->limit)){
|
|
||||||
$limit=$sqlQuery->limit['limit'];
|
|
||||||
if(isset($sqlQuery->limit['start']))
|
|
||||||
$offset=$sqlQuery->limit['start'];
|
|
||||||
|
|
||||||
} else if(preg_match('/^([0-9]+) offset ([0-9]+)$/i', trim($sqlQuery->limit), $matches)) {
|
|
||||||
$limit = $matches[1];
|
|
||||||
$offset = $matches[2];
|
|
||||||
} else {
|
|
||||||
//could be a comma delimited string
|
|
||||||
$bits=explode(',', $sqlQuery->limit);
|
|
||||||
if(sizeof($bits) > 1) {
|
|
||||||
list($offset, $limit) = $bits;
|
|
||||||
} else {
|
|
||||||
$limit = $bits[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$text = '';
|
|
||||||
$suffixText = '';
|
|
||||||
$nestedQuery = false;
|
|
||||||
|
|
||||||
// DELETE queries
|
|
||||||
if($sqlQuery->delete) {
|
|
||||||
$text = 'DELETE ';
|
|
||||||
|
|
||||||
// SELECT queries
|
|
||||||
} else {
|
|
||||||
$distinct = $sqlQuery->distinct ? "DISTINCT " : "";
|
|
||||||
|
|
||||||
// If there's a limit but no offset, just use 'TOP X'
|
|
||||||
// rather than the more complex sub-select method
|
|
||||||
if ($limit != 0 && $offset == 0) {
|
|
||||||
$text = "SELECT $distinct TOP $limit";
|
|
||||||
|
|
||||||
// If there's a limit and an offset, then we need to do a subselect
|
|
||||||
} else if($limit && $offset) {
|
|
||||||
if($sqlQuery->orderby) {
|
|
||||||
$rowNumber = "ROW_NUMBER() OVER (ORDER BY $sqlQuery->orderby) AS Number";
|
|
||||||
} else {
|
|
||||||
$firstCol = reset($sqlQuery->select);
|
|
||||||
$rowNumber = "ROW_NUMBER() OVER (ORDER BY $firstCol) AS Number";
|
|
||||||
}
|
|
||||||
$text = "SELECT * FROM ( SELECT $distinct$rowNumber, ";
|
|
||||||
$suffixText .= ") AS Numbered WHERE Number BETWEEN " . ($offset+1) ." AND " . ($offset+$limit)
|
|
||||||
. " ORDER BY Number";
|
|
||||||
$nestedQuery = true;
|
|
||||||
|
|
||||||
// Otherwise a simple query
|
|
||||||
} else {
|
|
||||||
$text = "SELECT $distinct";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now add the columns to be selected
|
|
||||||
$text .= implode(", ", $sqlQuery->select);
|
|
||||||
}
|
|
||||||
|
|
||||||
$text .= " FROM " . implode(" ", $sqlQuery->from);
|
|
||||||
if($sqlQuery->where) $text .= " WHERE (" . $sqlQuery->getFilter(). ")";
|
|
||||||
if($sqlQuery->groupby) $text .= " GROUP BY " . implode(", ", $sqlQuery->groupby);
|
|
||||||
if($sqlQuery->having) $text .= " HAVING ( " . implode(" ) AND ( ", $sqlQuery->having) . " )";
|
|
||||||
if(!$nestedQuery && $sqlQuery->orderby) $text .= " ORDER BY " . $sqlQuery->orderby;
|
|
||||||
|
|
||||||
// $suffixText is used by the nested queries to create an offset limit
|
|
||||||
if($suffixText) $text .= $suffixText;
|
|
||||||
|
|
||||||
return $text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This will return text which has been escaped in a database-friendly manner
|
|
||||||
* Using PHP's addslashes method won't work in MSSQL
|
|
||||||
*/
|
|
||||||
function addslashes($value){
|
|
||||||
$value=stripslashes($value);
|
|
||||||
$value=str_replace("'","''",$value);
|
|
||||||
$value=str_replace("\0","[NULL]",$value);
|
|
||||||
|
|
||||||
return $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This changes the index name depending on database requirements.
|
|
||||||
* MSSQL requires underscores to be replaced with commas.
|
|
||||||
*/
|
|
||||||
function modifyIndex($index) {
|
|
||||||
return str_replace('_', ',', $index);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The core search engine configuration.
|
* The core search engine configuration.
|
||||||
* @todo There is no result relevancy or ordering as it currently stands.
|
* Picks up the fulltext-indexed tables from the database and executes search on all of them.
|
||||||
|
* Results are obtained as ID-ClassName pairs which is later used to reconstruct the DataObjectSet.
|
||||||
*
|
*
|
||||||
|
* @param array $classesToSearch computes all descendants and includes them. Check is done via WHERE clause.
|
||||||
* @param string $keywords Keywords as a space separated string
|
* @param string $keywords Keywords as a space separated string
|
||||||
* @return object DataObjectSet of result pages
|
* @param int $start
|
||||||
|
* @param int $pageLength
|
||||||
|
* @param string $sortBy
|
||||||
|
* @param string $extraFilter
|
||||||
|
* @param bool $booleanSearch
|
||||||
|
* @param string $alternativeFileFilter
|
||||||
|
* @param bool $invertedMatch
|
||||||
|
* @return PaginatedList DataObjectSet of result pages
|
||||||
*/
|
*/
|
||||||
public function searchEngine($classesToSearch, $keywords, $start, $pageLength, $sortBy = "Relevance DESC", $extraFilter = "", $booleanSearch = false, $alternativeFileFilter = "", $invertedMatch = false) {
|
public function searchEngine($classesToSearch, $keywords, $start, $pageLength, $sortBy = "Relevance DESC", $extraFilter = "", $booleanSearch = false, $alternativeFileFilter = "", $invertedMatch = false)
|
||||||
$results = new DataObjectSet();
|
{
|
||||||
if(!$this->fullTextEnabled()) return $results;
|
$start = (int)$start;
|
||||||
if (substr($sortBy, 0, 9)!='Relevance') user_error("Non-relevance sort not supported.", E_USER_ERROR);
|
$pageLength = (int)$pageLength;
|
||||||
|
$results = new ArrayList();
|
||||||
|
|
||||||
|
if (!$this->fullTextEnabled()) {
|
||||||
|
return new PaginatedList($results);
|
||||||
|
}
|
||||||
|
if (!in_array(substr($sortBy, 0, 9), array('"Relevanc', 'Relevance'))) {
|
||||||
|
user_error("Non-relevance sort not supported.", E_USER_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
$allClassesToSearch = array();
|
||||||
|
foreach ($classesToSearch as $class) {
|
||||||
|
$allClassesToSearch = array_merge($allClassesToSearch, array_values(ClassInfo::dataClassesFor($class)));
|
||||||
|
}
|
||||||
|
$allClassesToSearch = array_unique($allClassesToSearch);
|
||||||
|
|
||||||
//Get a list of all the tables and columns we'll be searching on:
|
//Get a list of all the tables and columns we'll be searching on:
|
||||||
$fulltextColumns = DB::query('EXEC sp_help_fulltext_columns');
|
$fulltextColumns = $this->query('EXEC sp_help_fulltext_columns');
|
||||||
$queries = array();
|
$queries = array();
|
||||||
|
|
||||||
// Sort the columns back into tables.
|
// Sort the columns back into tables.
|
||||||
$tables = array();
|
$tables = array();
|
||||||
foreach ($fulltextColumns as $column) {
|
foreach ($fulltextColumns as $column) {
|
||||||
// Skip extension tables.
|
// Skip extension tables.
|
||||||
if(substr($column['TABLE_NAME'], -5)=='_Live' || substr($column['TABLE_NAME'], -9)=='_versions') continue;
|
if (substr($column['TABLE_NAME'], -5) == '_Live' || substr($column['TABLE_NAME'], -9) == '_versions') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Add the column to table.
|
// Add the column to table.
|
||||||
$table = &$tables[$column['TABLE_NAME']];
|
$table = &$tables[$column['TABLE_NAME']];
|
||||||
if (!$table) $table = array($column['FULLTEXT_COLUMN_NAME']);
|
if (!$table) {
|
||||||
else array_push($table, $column['FULLTEXT_COLUMN_NAME']);
|
$table = array($column['FULLTEXT_COLUMN_NAME']);
|
||||||
|
} else {
|
||||||
|
array_push($table, $column['FULLTEXT_COLUMN_NAME']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create one query per each table, columns not used.
|
// Create one query per each table, $columns not used. We want just the ID and the ClassName of the object from this query.
|
||||||
foreach ($tables as $tableName => $columns) {
|
foreach ($tables as $tableName => $columns) {
|
||||||
|
$class = DataObject::getSchema()->tableClass($tableName);
|
||||||
$join = $this->fullTextSearchMSSQL($tableName, $keywords);
|
$join = $this->fullTextSearchMSSQL($tableName, $keywords);
|
||||||
if (!$join) return new DataObjectSet(); // avoid "Null or empty full-text predicate"
|
if (!$join) {
|
||||||
|
return new PaginatedList($results);
|
||||||
|
} // avoid "Null or empty full-text predicate"
|
||||||
|
|
||||||
// Check if we need to add ShowInSearch
|
// Check if we need to add ShowInSearch
|
||||||
$where = null;
|
$where = null;
|
||||||
if(strpos($tableName, 'SiteTree') === 0) {
|
if ($class === 'SilverStripe\\CMS\\Model\\SiteTree') {
|
||||||
|
$where = array("\"$tableName\".\"ShowInSearch\"!=0");
|
||||||
|
} elseif ($class === 'SilverStripe\\Assets\\File') {
|
||||||
|
// File.ShowInSearch was added later, keep the database driver backwards compatible
|
||||||
|
// by checking for its existence first
|
||||||
|
$fields = $this->getSchemaManager()->fieldList($tableName);
|
||||||
|
if (array_key_exists('ShowInSearch', $fields)) {
|
||||||
$where = array("\"$tableName\".\"ShowInSearch\"!=0");
|
$where = array("\"$tableName\".\"ShowInSearch\"!=0");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$queries[$tableName] = DataList::create($class)->where($where)->dataQuery()->query();
|
||||||
|
$queries[$tableName]->setOrderBy(array());
|
||||||
|
|
||||||
// Join with CONTAINSTABLE, a full text searcher that includes relevance factor
|
// Join with CONTAINSTABLE, a full text searcher that includes relevance factor
|
||||||
$queries[$tableName] = singleton($tableName)->extendedSQL($where);
|
$queries[$tableName]->setFrom(array("\"$tableName\" INNER JOIN $join AS \"ft\" ON \"$tableName\".\"ID\"=\"ft\".\"KEY\""));
|
||||||
$queries[$tableName]->from = array("\"$tableName\" INNER JOIN $join AS \"ft\" ON \"$tableName\".\"ID\"=\"ft\".\"KEY\"");
|
// Join with the base class if needed, as we want to test agains the ClassName
|
||||||
$queries[$tableName]->select = array("\"$tableName\".\"ID\"", "'$tableName' AS Source", "\"Rank\" AS \"Relevance\"");
|
if ($tableName != $tableName) {
|
||||||
$queries[$tableName]->orderby = null;
|
$queries[$tableName]->setFrom("INNER JOIN \"$tableName\" ON \"$tableName\".\"ID\"=\"$tableName\".\"ID\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
$queries[$tableName]->setSelect(array("\"$tableName\".\"ID\""));
|
||||||
|
$queries[$tableName]->selectField("'$tableName'", 'Source');
|
||||||
|
$queries[$tableName]->selectField('Rank', 'Relevance');
|
||||||
|
if ($extraFilter) {
|
||||||
|
$queries[$tableName]->addWhere($extraFilter);
|
||||||
|
}
|
||||||
|
if (count($allClassesToSearch)) {
|
||||||
|
$classesPlaceholder = DB::placeholders($allClassesToSearch);
|
||||||
|
$queries[$tableName]->addWhere(array(
|
||||||
|
"\"$tableName\".\"ClassName\" IN ($classesPlaceholder)" =>
|
||||||
|
$allClassesToSearch
|
||||||
|
));
|
||||||
|
}
|
||||||
|
// Reset the parameters that would get in the way
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate SQL
|
// Generate SQL
|
||||||
$querySQLs = array();
|
$querySQLs = array();
|
||||||
|
$queryParameters = array();
|
||||||
foreach ($queries as $query) {
|
foreach ($queries as $query) {
|
||||||
$querySQLs[] = $query->sql();
|
/** @var SQLSelect $query */
|
||||||
|
$querySQLs[] = $query->sql($parameters);
|
||||||
|
$queryParameters = array_merge($queryParameters, $parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unite the SQL
|
// Unite the SQL
|
||||||
$fullQuery = implode(" UNION ", $querySQLs) . " ORDER BY $sortBy";
|
$fullQuery = implode(" UNION ", $querySQLs) . " ORDER BY $sortBy";
|
||||||
|
|
||||||
// Perform the search
|
// Perform the search
|
||||||
$result = DB::query($fullQuery);
|
$result = $this->preparedQuery($fullQuery, $queryParameters);
|
||||||
|
|
||||||
// Regenerate DataObjectSet - watch out, numRecords doesn't work on sqlsrv driver on Windows.
|
// Regenerate DataObjectSet - watch out, numRecords doesn't work on sqlsrv driver on Windows.
|
||||||
$current = -1;
|
$current = -1;
|
||||||
$results = new DataObjectSet();
|
$objects = array();
|
||||||
foreach ($result as $row) {
|
foreach ($result as $row) {
|
||||||
$current++;
|
$current++;
|
||||||
|
|
||||||
// Select a subset for paging
|
// Select a subset for paging
|
||||||
if ($current >= $start && $current < $start + $pageLength) {
|
if ($current >= $start && $current < $start + $pageLength) {
|
||||||
$results->push(DataObject::get_by_id($row['Source'], $row['ID']));
|
$objects[] = DataObject::get_by_id($row['Source'], $row['ID']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$results->setPageLimits($start, $pageLength, $current+1);
|
|
||||||
|
|
||||||
return $results;
|
if (isset($objects)) {
|
||||||
|
$results = new ArrayList($objects);
|
||||||
|
} else {
|
||||||
|
$results = new ArrayList();
|
||||||
|
}
|
||||||
|
$list = new PaginatedList($results);
|
||||||
|
$list->setPageStart($start);
|
||||||
|
$list->setPageLength($pageLength);
|
||||||
|
$list->setTotalItems($current+1);
|
||||||
|
return $list;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allow auto-increment primary key editing on the given table.
|
* Allow auto-increment primary key editing on the given table.
|
||||||
* Some databases need to enable this specially.
|
* Some databases need to enable this specially.
|
||||||
* @param $table The name of the table to have PK editing allowed on
|
*
|
||||||
* @param $allow True to start, false to finish
|
* @param string $table The name of the table to have PK editing allowed on
|
||||||
|
* @param bool $allow True to start, false to finish
|
||||||
*/
|
*/
|
||||||
function allowPrimaryKeyEditing($table, $allow = true) {
|
public function allowPrimaryKeyEditing($table, $allow = true)
|
||||||
|
{
|
||||||
$this->query("SET IDENTITY_INSERT \"$table\" " . ($allow ? "ON" : "OFF"));
|
$this->query("SET IDENTITY_INSERT \"$table\" " . ($allow ? "ON" : "OFF"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a fulltext index exists on a particular table name.
|
|
||||||
* @return boolean TRUE index exists | FALSE index does not exist | NULL no support
|
|
||||||
*/
|
|
||||||
function fulltextIndexExists($tableName) {
|
|
||||||
// Special case for no full text index support
|
|
||||||
if(!$this->fullTextEnabled()) return null;
|
|
||||||
return (bool) $this->query("
|
|
||||||
SELECT 1 FROM sys.fulltext_indexes i
|
|
||||||
JOIN sys.objects o ON i.object_id = o.object_id
|
|
||||||
WHERE o.name = '$tableName'
|
|
||||||
")->value();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a SQL fragment for querying a fulltext search index
|
* Returns a SQL fragment for querying a fulltext search index
|
||||||
*
|
*
|
||||||
* @param $tableName specific - table name
|
* @param string $tableName specific - table name
|
||||||
* @param $keywords string The search query
|
* @param string $keywords The search query
|
||||||
* @param $fields array The list of field names to search on, or null to include all
|
* @param array $fields The list of field names to search on, or null to include all
|
||||||
*
|
* @return string Clause, or null if keyword set is empty or the string with JOIN clause to be added to SQL query
|
||||||
* @returns null if keyword set is empty or the string with JOIN clause to be added to SQL query
|
|
||||||
*/
|
*/
|
||||||
function fullTextSearchMSSQL($tableName, $keywords, $fields = null) {
|
public function fullTextSearchMSSQL($tableName, $keywords, $fields = null)
|
||||||
|
{
|
||||||
// Make sure we are getting an array of fields
|
// Make sure we are getting an array of fields
|
||||||
if (isset($fields) && !is_array($fields)) $fields = array($fields);
|
if (isset($fields) && !is_array($fields)) {
|
||||||
|
$fields = array($fields);
|
||||||
|
}
|
||||||
|
|
||||||
// Strip unfriendly characters, SQLServer "CONTAINS" predicate will crash on & and | and ignore others anyway.
|
// Strip unfriendly characters, SQLServer "CONTAINS" predicate will crash on & and | and ignore others anyway.
|
||||||
if (function_exists('mb_ereg_replace')) {
|
if (function_exists('mb_ereg_replace')) {
|
||||||
$keywords = mb_ereg_replace('[^\w\s]', '', trim($keywords));
|
$keywords = mb_ereg_replace('[^\w\s]', '', trim($keywords));
|
||||||
}
|
} else {
|
||||||
else {
|
$keywords = $this->escapeString(str_replace(array('&', '|', '!', '"', '\''), '', trim($keywords)));
|
||||||
$keywords = Convert::raw2sql(str_replace(array('&','|','!','"','\''), '', trim($keywords)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove stopwords, concat with ANDs
|
// Remove stopwords, concat with ANDs
|
||||||
$keywords = explode(' ', $keywords);
|
$keywordList = explode(' ', $keywords);
|
||||||
$keywords = self::removeStopwords($keywords);
|
$keywordList = $this->removeStopwords($keywordList);
|
||||||
$keywords = implode(' AND ', $keywords);
|
|
||||||
|
|
||||||
if (!$keywords || trim($keywords)=='') return null;
|
// remove any empty values from the array
|
||||||
|
$keywordList = array_filter($keywordList);
|
||||||
|
if (empty($keywordList)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if ($fields) $fieldNames = '"' . implode('", "', $fields) . '"';
|
$keywords = implode(' AND ', $keywordList);
|
||||||
else $fieldNames = "*";
|
if ($fields) {
|
||||||
|
$fieldNames = '"' . implode('", "', $fields) . '"';
|
||||||
|
} else {
|
||||||
|
$fieldNames = "*";
|
||||||
|
}
|
||||||
|
|
||||||
return "FREETEXTTABLE(\"$tableName\", ($fieldNames), '$keywords')";
|
return "CONTAINSTABLE(\"$tableName\", ($fieldNames), '$keywords')";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1358,107 +408,183 @@ class MSSQLDatabase extends SS_Database {
|
|||||||
*
|
*
|
||||||
* @return array $keywords with stopwords removed
|
* @return array $keywords with stopwords removed
|
||||||
*/
|
*/
|
||||||
static public function removeStopwords($keywords) {
|
public function removeStopwords($keywords)
|
||||||
|
{
|
||||||
$goodKeywords = array();
|
$goodKeywords = array();
|
||||||
foreach ($keywords as $keyword) {
|
foreach ($keywords as $keyword) {
|
||||||
if (in_array($keyword, self::$noiseWords)) continue;
|
if (in_array($keyword, self::$noiseWords)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
$goodKeywords[] = trim($keyword);
|
$goodKeywords[] = trim($keyword);
|
||||||
}
|
}
|
||||||
return $goodKeywords;
|
return $goodKeywords;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/**
|
||||||
* Does this database support transactions?
|
* Does this database support transactions?
|
||||||
*/
|
*/
|
||||||
public function supportsTransactions(){
|
public function supportsTransactions()
|
||||||
|
{
|
||||||
return $this->supportsTransactions;
|
return $this->supportsTransactions;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/**
|
||||||
* This is a quick lookup to discover if the database supports particular extensions
|
* This is a quick lookup to discover if the database supports particular extensions
|
||||||
* Currently, MSSQL supports no extensions
|
* Currently, MSSQL supports no extensions
|
||||||
|
*
|
||||||
|
* @param array $extensions List of extensions to check for support of. The key of this array
|
||||||
|
* will be an extension name, and the value the configuration for that extension. This
|
||||||
|
* could be one of partitions, tablespaces, or clustering
|
||||||
|
* @return boolean Flag indicating support for all of the above
|
||||||
*/
|
*/
|
||||||
public function supportsExtensions($extensions=Array('partitions', 'tablespaces', 'clustering')){
|
public function supportsExtensions($extensions = array('partitions', 'tablespaces', 'clustering'))
|
||||||
if(isset($extensions['partitions']))
|
{
|
||||||
|
if (isset($extensions['partitions'])) {
|
||||||
return false;
|
return false;
|
||||||
elseif(isset($extensions['tablespaces']))
|
} elseif (isset($extensions['tablespaces'])) {
|
||||||
return false;
|
return false;
|
||||||
elseif(isset($extensions['clustering']))
|
} elseif (isset($extensions['clustering'])) {
|
||||||
return false;
|
return false;
|
||||||
else
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/**
|
||||||
* Start transaction. READ ONLY not supported.
|
* Start transaction. READ ONLY not supported.
|
||||||
|
*
|
||||||
|
* @param bool $transactionMode
|
||||||
|
* @param bool $sessionCharacteristics
|
||||||
*/
|
*/
|
||||||
public function startTransaction($transaction_mode=false, $session_characteristics=false){
|
public function transactionStart($transactionMode = false, $sessionCharacteristics = false)
|
||||||
if($this->mssql) {
|
{
|
||||||
DB::query('BEGIN TRANSACTION');
|
if ($this->transactionNesting > 0) {
|
||||||
|
$this->transactionSavepoint('NESTEDTRANSACTION' . $this->transactionNesting);
|
||||||
|
} elseif ($this->connector instanceof SQLServerConnector) {
|
||||||
|
$this->connector->transactionStart();
|
||||||
} else {
|
} else {
|
||||||
$result = sqlsrv_begin_transaction($this->dbConn);
|
$this->query('BEGIN TRANSACTION');
|
||||||
if (!$result) $this->databaseError("Couldn't start the transaction.", E_USER_ERROR);
|
|
||||||
}
|
}
|
||||||
|
++$this->transactionNesting;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
public function transactionSavepoint($savepoint)
|
||||||
* Create a savepoint that you can jump back to if you encounter problems
|
{
|
||||||
*/
|
$this->query("SAVE TRANSACTION \"$savepoint\"");
|
||||||
public function transactionSavepoint($savepoint){
|
|
||||||
// The savepoints seem to work on FreeTDS, but throw error anyway to avoid
|
|
||||||
// nasty surprises upon deployment from LAMP to Windows.
|
|
||||||
$this->databaseError("Savepoints currently not supported.", E_USER_ERROR);
|
|
||||||
return;
|
|
||||||
|
|
||||||
if($this->mssql) {
|
|
||||||
DB::query("SAVE TRANSACTION \"$savepoint\"");
|
|
||||||
} else {
|
|
||||||
$this->databaseError("Savepoints currently not supported.", E_USER_ERROR);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
public function transactionRollback($savepoint = false)
|
||||||
* Rollback or revert to a savepoint if your queries encounter problems
|
{
|
||||||
* If you encounter a problem at any point during a transaction, you may
|
// Named transaction
|
||||||
* need to rollback that particular query, or return to a savepoint
|
|
||||||
*/
|
|
||||||
public function transactionRollback($savepoint=false){
|
|
||||||
if ($savepoint) {
|
if ($savepoint) {
|
||||||
// The savepoints seem to work on FreeTDS, but throw error anyway to avoid
|
$this->query("ROLLBACK TRANSACTION \"$savepoint\"");
|
||||||
// nasty surprises upon deployment from LAMP to Windows.
|
return true;
|
||||||
$this->databaseError("Savepoints currently not supported.", E_USER_ERROR);
|
|
||||||
return;
|
|
||||||
|
|
||||||
if($this->mssql) {
|
|
||||||
DB::query("ROLLBACK TRANSACTION \"$savepoint\"");
|
|
||||||
} else {
|
|
||||||
$this->databaseError("Savepoints currently not supported.", E_USER_ERROR);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if($this->mssql) {
|
|
||||||
DB::query('ROLLBACK TRANSACTION');
|
|
||||||
} else {
|
|
||||||
$result = sqlsrv_rollback($this->dbConn);
|
|
||||||
if (!$result) $this->databaseError("Couldn't rollback the transaction.", E_USER_ERROR);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
// Fail if transaction isn't available
|
||||||
* Commit everything inside this transaction so far
|
if (!$this->transactionNesting) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
--$this->transactionNesting;
|
||||||
|
if ($this->transactionNesting > 0) {
|
||||||
|
$this->transactionRollback('NESTEDTRANSACTION' . $this->transactionNesting);
|
||||||
|
} elseif ($this->connector instanceof SQLServerConnector) {
|
||||||
|
$this->connector->transactionRollback();
|
||||||
|
} else {
|
||||||
|
$this->query('ROLLBACK TRANSACTION');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function transactionEnd($chain = false)
|
||||||
|
{
|
||||||
|
// Fail if transaction isn't available
|
||||||
|
if (!$this->transactionNesting) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
--$this->transactionNesting;
|
||||||
|
if ($this->transactionNesting <= 0) {
|
||||||
|
$this->transactionNesting = 0;
|
||||||
|
if ($this->connector instanceof SQLServerConnector) {
|
||||||
|
$this->connector->transactionEnd();
|
||||||
|
} else {
|
||||||
|
$this->query('COMMIT TRANSACTION');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In error condition, set transactionNesting to zero
|
||||||
*/
|
*/
|
||||||
public function endTransaction(){
|
protected function resetTransactionNesting()
|
||||||
if($this->mssql) {
|
{
|
||||||
DB::query('COMMIT TRANSACTION');
|
$this->transactionNesting = 0;
|
||||||
} else {
|
|
||||||
$result = sqlsrv_commit($this->dbConn);
|
|
||||||
if (!$result) $this->databaseError("Couldn't commit the transaction.", E_USER_ERROR);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function query($sql, $errorLevel = E_USER_ERROR)
|
||||||
|
{
|
||||||
|
$this->inspectQuery($sql);
|
||||||
|
return parent::query($sql, $errorLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR)
|
||||||
|
{
|
||||||
|
$this->inspectQuery($sql);
|
||||||
|
return parent::preparedQuery($sql, $parameters, $errorLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function inspectQuery($sql)
|
||||||
|
{
|
||||||
|
// Any DDL discards transactions.
|
||||||
|
$isDDL = $this->getConnector()->isQueryDDL($sql);
|
||||||
|
if ($isDDL) {
|
||||||
|
$this->resetTransactionNesting();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function comparisonClause($field, $value, $exact = false, $negate = false, $caseSensitive = null, $parameterised = false)
|
||||||
|
{
|
||||||
|
if ($exact) {
|
||||||
|
$comp = ($negate) ? '!=' : '=';
|
||||||
|
} else {
|
||||||
|
$comp = 'LIKE';
|
||||||
|
if ($negate) {
|
||||||
|
$comp = 'NOT ' . $comp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Field definitions are case insensitive by default,
|
||||||
|
// change used collation for case sensitive searches.
|
||||||
|
$collateClause = '';
|
||||||
|
if ($caseSensitive === true) {
|
||||||
|
if (self::get_collation()) {
|
||||||
|
$collation = preg_replace('/_CI_/', '_CS_', self::get_collation());
|
||||||
|
} else {
|
||||||
|
$collation = 'Latin1_General_CS_AS';
|
||||||
|
}
|
||||||
|
$collateClause = ' COLLATE ' . $collation;
|
||||||
|
} elseif ($caseSensitive === false) {
|
||||||
|
if (self::get_collation()) {
|
||||||
|
$collation = preg_replace('/_CS_/', '_CI_', self::get_collation());
|
||||||
|
} else {
|
||||||
|
$collation = 'Latin1_General_CI_AS';
|
||||||
|
}
|
||||||
|
$collateClause = ' COLLATE ' . $collation;
|
||||||
|
}
|
||||||
|
|
||||||
|
$clause = sprintf("%s %s %s", $field, $comp, $parameterised ? '?' : "'$value'");
|
||||||
|
if ($collateClause) {
|
||||||
|
$clause .= $collateClause;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $clause;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function to return an SQL datetime expression for MSSQL
|
* Function to return an SQL datetime expression for MSSQL
|
||||||
* used for querying a datetime in a certain format
|
* used for querying a datetime in a certain format
|
||||||
|
*
|
||||||
* @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"'
|
* @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"'
|
||||||
* @param string $format to be used, supported specifiers:
|
* @param string $format to be used, supported specifiers:
|
||||||
* %Y = Year (four digits)
|
* %Y = Year (four digits)
|
||||||
@ -1470,9 +596,14 @@ class MSSQLDatabase extends SS_Database {
|
|||||||
* %U = unix timestamp, can only be used on it's own
|
* %U = unix timestamp, can only be used on it's own
|
||||||
* @return string SQL datetime expression to query for a formatted datetime
|
* @return string SQL datetime expression to query for a formatted datetime
|
||||||
*/
|
*/
|
||||||
function formattedDatetimeClause($date, $format) {
|
public function formattedDatetimeClause($date, $format)
|
||||||
|
{
|
||||||
preg_match_all('/%(.)/', $format, $matches);
|
preg_match_all('/%(.)/', $format, $matches);
|
||||||
foreach($matches[1] as $match) if(array_search($match, array('Y','m','d','H','i','s','U')) === false) user_error('formattedDatetimeClause(): unsupported format character %' . $match, E_USER_WARNING);
|
foreach ($matches[1] as $match) {
|
||||||
|
if (array_search($match, array('Y', 'm', 'd', 'H', 'i', 's', 'U')) === false) {
|
||||||
|
user_error('formattedDatetimeClause(): unsupported format character %' . $match, E_USER_WARNING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (preg_match('/^now$/i', $date)) {
|
if (preg_match('/^now$/i', $date)) {
|
||||||
$date = "CURRENT_TIMESTAMP";
|
$date = "CURRENT_TIMESTAMP";
|
||||||
@ -1480,7 +611,9 @@ class MSSQLDatabase extends SS_Database {
|
|||||||
$date = "'$date.000'";
|
$date = "'$date.000'";
|
||||||
}
|
}
|
||||||
|
|
||||||
if($format == '%U') return "DATEDIFF(s, '19700101 12:00:00:000', $date)";
|
if ($format == '%U') {
|
||||||
|
return "DATEDIFF(s, '1970-01-01 00:00:00', DATEADD(hour, DATEDIFF(hour, GETDATE(), GETUTCDATE()), $date))";
|
||||||
|
}
|
||||||
|
|
||||||
$trans = array(
|
$trans = array(
|
||||||
'Y' => 'yy',
|
'Y' => 'yy',
|
||||||
@ -1512,12 +645,12 @@ class MSSQLDatabase extends SS_Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return '(' . implode(' + ', $strings) . ')';
|
return '(' . implode(' + ', $strings) . ')';
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function to return an SQL datetime expression for MSSQL.
|
* Function to return an SQL datetime expression for MSSQL.
|
||||||
* used for querying a datetime addition
|
* used for querying a datetime addition
|
||||||
|
*
|
||||||
* @param string $date, can be either 'now', literal datetime like '1973-10-14 10:30:00' or field name, e.g. '"SiteTree"."Created"'
|
* @param string $date, can be either 'now', literal datetime like '1973-10-14 10:30:00' or field name, e.g. '"SiteTree"."Created"'
|
||||||
* @param string $interval to be added, use the format [sign][integer] [qualifier], e.g. -1 Day, +15 minutes, +1 YEAR
|
* @param string $interval to be added, use the format [sign][integer] [qualifier], e.g. -1 Day, +15 minutes, +1 YEAR
|
||||||
* supported qualifiers:
|
* supported qualifiers:
|
||||||
@ -1530,7 +663,8 @@ class MSSQLDatabase extends SS_Database {
|
|||||||
* This includes the singular forms as well
|
* This includes the singular forms as well
|
||||||
* @return string SQL datetime expression to query for a datetime (YYYY-MM-DD hh:mm:ss) which is the result of the addition
|
* @return string SQL datetime expression to query for a datetime (YYYY-MM-DD hh:mm:ss) which is the result of the addition
|
||||||
*/
|
*/
|
||||||
function datetimeIntervalClause($date, $interval) {
|
public function datetimeIntervalClause($date, $interval)
|
||||||
|
{
|
||||||
$trans = array(
|
$trans = array(
|
||||||
'year' => 'yy',
|
'year' => 'yy',
|
||||||
'month' => 'mm',
|
'month' => 'mm',
|
||||||
@ -1545,7 +679,9 @@ class MSSQLDatabase extends SS_Database {
|
|||||||
if (
|
if (
|
||||||
!($params = preg_match('/([-+]\d+) (\w+)/i', $singularinterval, $matches)) ||
|
!($params = preg_match('/([-+]\d+) (\w+)/i', $singularinterval, $matches)) ||
|
||||||
!isset($trans[strtolower($matches[2])])
|
!isset($trans[strtolower($matches[2])])
|
||||||
) user_error('datetimeIntervalClause(): invalid interval ' . $interval, E_USER_WARNING);
|
) {
|
||||||
|
user_error('datetimeIntervalClause(): invalid interval ' . $interval, E_USER_WARNING);
|
||||||
|
}
|
||||||
|
|
||||||
if (preg_match('/^now$/i', $date)) {
|
if (preg_match('/^now$/i', $date)) {
|
||||||
$date = "CURRENT_TIMESTAMP";
|
$date = "CURRENT_TIMESTAMP";
|
||||||
@ -1559,12 +695,13 @@ class MSSQLDatabase extends SS_Database {
|
|||||||
/**
|
/**
|
||||||
* Function to return an SQL datetime expression for MSSQL.
|
* Function to return an SQL datetime expression for MSSQL.
|
||||||
* used for querying a datetime substraction
|
* used for querying a datetime substraction
|
||||||
|
*
|
||||||
* @param string $date1, can be either 'now', literal datetime like '1973-10-14 10:30:00' or field name, e.g. '"SiteTree"."Created"'
|
* @param string $date1, can be either 'now', literal datetime like '1973-10-14 10:30:00' or field name, e.g. '"SiteTree"."Created"'
|
||||||
* @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"'
|
* @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"'
|
||||||
* @return string SQL datetime expression to query for the interval between $date1 and $date2 in seconds which is the result of the substraction
|
* @return string SQL datetime expression to query for the interval between $date1 and $date2 in seconds which is the result of the substraction
|
||||||
*/
|
*/
|
||||||
function datetimeDifferenceClause($date1, $date2) {
|
public function datetimeDifferenceClause($date1, $date2)
|
||||||
|
{
|
||||||
if (preg_match('/^now$/i', $date1)) {
|
if (preg_match('/^now$/i', $date1)) {
|
||||||
$date1 = "CURRENT_TIMESTAMP";
|
$date1 = "CURRENT_TIMESTAMP";
|
||||||
} elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date1)) {
|
} elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date1)) {
|
||||||
@ -1580,121 +717,3 @@ class MSSQLDatabase extends SS_Database {
|
|||||||
return "DATEDIFF(s, $date2, $date1)";
|
return "DATEDIFF(s, $date2, $date1)";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A result-set from a MSSQL database.
|
|
||||||
* @package sapphire
|
|
||||||
* @subpackage model
|
|
||||||
*/
|
|
||||||
class MSSQLQuery extends SS_Query {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The MSSQLDatabase object that created this result set.
|
|
||||||
* @var MSSQLDatabase
|
|
||||||
*/
|
|
||||||
private $database;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The internal MSSQL handle that points to the result set.
|
|
||||||
* @var resource
|
|
||||||
*/
|
|
||||||
private $handle;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If true, use the mssql_... functions.
|
|
||||||
* If false use the sqlsrv_... functions
|
|
||||||
*/
|
|
||||||
private $mssql = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook the result-set given into a Query class, suitable for use by sapphire.
|
|
||||||
* @param database The database object that created this query.
|
|
||||||
* @param handle the internal mssql handle that is points to the resultset.
|
|
||||||
*/
|
|
||||||
public function __construct(MSSQLDatabase $database, $handle, $mssql) {
|
|
||||||
$this->database = $database;
|
|
||||||
$this->handle = $handle;
|
|
||||||
$this->mssql = $mssql;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function __destroy() {
|
|
||||||
if($this->mssql) {
|
|
||||||
mssql_free_result($this->handle);
|
|
||||||
} else {
|
|
||||||
sqlsrv_free_stmt($this->handle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function seek($row) {
|
|
||||||
if($this->mssql) {
|
|
||||||
return mssql_data_seek($this->handle, $row);
|
|
||||||
} else {
|
|
||||||
user_error("MSSQLQuery::seek() sqlsrv doesn't support seek.", E_USER_WARNING);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function numRecords() {
|
|
||||||
if(!is_resource($this->handle)) return false;
|
|
||||||
if($this->mssql) {
|
|
||||||
return mssql_num_rows($this->handle);
|
|
||||||
} else {
|
|
||||||
// WARNING: This will only work if the cursor type is NOT forward only!
|
|
||||||
if(function_exists('sqlsrv_num_rows')) {
|
|
||||||
return sqlsrv_num_rows($this->handle);
|
|
||||||
} else {
|
|
||||||
user_error("MSSQLQuery::numRecords() not supported on this version of sqlsrv.", E_USER_WARNING);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function nextRecord() {
|
|
||||||
|
|
||||||
if(!is_resource($this->handle)) return false;
|
|
||||||
|
|
||||||
// Coalesce rather than replace common fields.
|
|
||||||
$output = array();
|
|
||||||
|
|
||||||
if($this->mssql) {
|
|
||||||
if($data = mssql_fetch_row($this->handle)) {
|
|
||||||
foreach($data as $columnIdx => $value) {
|
|
||||||
$columnName = mssql_field_name($this->handle, $columnIdx);
|
|
||||||
// There are many places in the framework that expect the ID to be a string, not a double
|
|
||||||
// Do not set this to an integer, or it will cause failures in many tests that expect a string
|
|
||||||
if($columnName == 'ID') $value = (string) $value;
|
|
||||||
// $value || !$ouput[$columnName] means that the *last* occurring value is shown
|
|
||||||
// !$ouput[$columnName] means that the *first* occurring value is shown
|
|
||||||
if(isset($value) || !isset($output[$columnName])) {
|
|
||||||
$output[$columnName] = $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $output;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if($this->handle && $data = sqlsrv_fetch_array($this->handle, SQLSRV_FETCH_NUMERIC)) {
|
|
||||||
$fields = sqlsrv_field_metadata($this->handle);
|
|
||||||
foreach($fields as $columnIdx => $field) {
|
|
||||||
$value = $data[$columnIdx];
|
|
||||||
if($value instanceof DateTime) $value = $value->format('Y-m-d H:i:s');
|
|
||||||
|
|
||||||
// $value || !$ouput[$columnName] means that the *last* occurring value is shown
|
|
||||||
// !$ouput[$columnName] means that the *first* occurring value is shown
|
|
||||||
if(isset($value) || !isset($output[$field['Name']])) {
|
|
||||||
$output[$field['Name']] = $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $output;
|
|
||||||
} else {
|
|
||||||
// Free the handle if there are no more results - sqlsrv crashes if there are too many handles
|
|
||||||
if($this->handle) {
|
|
||||||
sqlsrv_free_stmt($this->handle);
|
|
||||||
$this->handle = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
@ -1,95 +1,179 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\MSSQL;
|
||||||
|
|
||||||
|
use SilverStripe\Dev\Install\DatabaseAdapterRegistry;
|
||||||
|
use SilverStripe\Dev\Install\DatabaseConfigurationHelper;
|
||||||
|
use PDO;
|
||||||
|
use Exception;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is a helper class for the SS installer.
|
* This is a helper class for the SS installer.
|
||||||
*
|
*
|
||||||
* It does all the specific checking for MSSQLDatabase
|
* It does all the specific checking for MSSQLDatabase
|
||||||
* to ensure that the configuration is setup correctly.
|
* to ensure that the configuration is setup correctly.
|
||||||
*
|
|
||||||
* @package mssql
|
|
||||||
*/
|
*/
|
||||||
class MSSQLDatabaseConfigurationHelper implements DatabaseConfigurationHelper {
|
class MSSQLDatabaseConfigurationHelper implements DatabaseConfigurationHelper
|
||||||
|
{
|
||||||
|
|
||||||
/**
|
protected function isAzure($databaseConfig)
|
||||||
* Ensure that the database function for connectivity is available.
|
{
|
||||||
* If it is, we assume the PHP module for this database has been setup correctly.
|
/** @skipUpgrade */
|
||||||
*
|
return $databaseConfig['type'] === 'MSSQLAzureDatabase';
|
||||||
* @param array $databaseConfig Associative array of database configuration, e.g. "server", "username" etc
|
|
||||||
* @return boolean
|
|
||||||
*/
|
|
||||||
public function requireDatabaseFunctions($databaseConfig) {
|
|
||||||
return (function_exists('mssql_connect') || function_exists('sqlsrv_connect')) ? true : false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure that the database server exists.
|
* Create a connection of the appropriate type
|
||||||
* @param array $databaseConfig Associative array of db configuration, e.g. "server", "username" etc
|
*
|
||||||
* @return array Result - e.g. array('success' => true, 'error' => 'details of error')
|
* @skipUpgrade
|
||||||
|
* @param array $databaseConfig
|
||||||
|
* @param string $error Error message passed by value
|
||||||
|
* @return mixed|null Either the connection object, or null if error
|
||||||
*/
|
*/
|
||||||
public function requireDatabaseServer($databaseConfig) {
|
protected function createConnection($databaseConfig, &$error)
|
||||||
$success = false;
|
{
|
||||||
$error = '';
|
$error = null;
|
||||||
|
try {
|
||||||
if(function_exists('mssql_connect')) {
|
switch ($databaseConfig['type']) {
|
||||||
$conn = @mssql_connect($databaseConfig['server'], $databaseConfig['username'], $databaseConfig['password'], true);
|
case 'MSSQLDatabase':
|
||||||
} else {
|
case 'MSSQLAzureDatabase':
|
||||||
$conn = @sqlsrv_connect($databaseConfig['server'], array(
|
$parameters = array(
|
||||||
'UID' => $databaseConfig['username'],
|
'UID' => $databaseConfig['username'],
|
||||||
'PWD' => $databaseConfig['password']
|
'PWD' => $databaseConfig['password']
|
||||||
));
|
);
|
||||||
|
|
||||||
$errors = sqlsrv_errors();
|
// Azure has additional parameter requirements
|
||||||
if($errors) {
|
if ($this->isAzure($databaseConfig)) {
|
||||||
$error .= "\n";
|
$parameters['database'] = $databaseConfig['database'];
|
||||||
foreach($errors as $detail) {
|
$parameters['multipleactiveresultsets'] = 0;
|
||||||
$error .= "\n" . @$detail['message'] . "\n";
|
|
||||||
}
|
}
|
||||||
}
|
$conn = @sqlsrv_connect($databaseConfig['server'], $parameters);
|
||||||
}
|
|
||||||
|
|
||||||
if ($conn) {
|
if ($conn) {
|
||||||
$success = true;
|
return $conn;
|
||||||
} else {
|
|
||||||
$success = false;
|
|
||||||
if(!$error) $error = 'SQL Server requires a valid username and password to determine if the server exists.';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get error
|
||||||
|
if ($errors = sqlsrv_errors()) {
|
||||||
|
$error = '';
|
||||||
|
foreach ($errors as $detail) {
|
||||||
|
$error .= "{$detail['message']}\n";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$error = 'Unknown connection error';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
case 'MSSQLPDODatabase':
|
||||||
|
$driver = $this->getPDODriver();
|
||||||
|
if (!$driver) {
|
||||||
|
$error = 'No supported PDO driver';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// May throw a PDOException if fails
|
||||||
|
$conn = @new PDO($driver.':Server='.$databaseConfig['server'], $databaseConfig['username'], $databaseConfig['password']);
|
||||||
|
if ($conn) {
|
||||||
|
return $conn;
|
||||||
|
} else {
|
||||||
|
$error = 'Unknown connection error';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
$error = 'Invalid connection type: ' . $databaseConfig['type'];
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (Exception $ex) {
|
||||||
|
$error = $ex->getMessage();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get supported PDO driver
|
||||||
|
*
|
||||||
|
* @return null
|
||||||
|
*/
|
||||||
|
public static function getPDODriver() {
|
||||||
|
if (!class_exists('PDO')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
foreach(PDO::getAvailableDrivers() as $driver) {
|
||||||
|
if(in_array($driver, array('sqlsrv', 'dblib'))) {
|
||||||
|
return $driver;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to quote a string value
|
||||||
|
*
|
||||||
|
* @param mixed $conn Connection object/resource
|
||||||
|
* @param string $value Value to quote
|
||||||
|
* @return string Quoted string
|
||||||
|
*/
|
||||||
|
protected function quote($conn, $value)
|
||||||
|
{
|
||||||
|
if ($conn instanceof PDO) {
|
||||||
|
return $conn->quote($value);
|
||||||
|
} elseif (is_resource($conn)) {
|
||||||
|
$value = str_replace("'", "''", $value);
|
||||||
|
$value = str_replace("\0", "[NULL]", $value);
|
||||||
|
return "N'$value'";
|
||||||
|
} else {
|
||||||
|
user_error('Invalid database connection', E_USER_ERROR);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to execute a query
|
||||||
|
*
|
||||||
|
* @param mixed $conn Connection object/resource
|
||||||
|
* @param string $sql SQL string to execute
|
||||||
|
* @return array List of first value from each resulting row
|
||||||
|
*/
|
||||||
|
protected function query($conn, $sql)
|
||||||
|
{
|
||||||
|
$items = array();
|
||||||
|
if ($conn instanceof PDO) {
|
||||||
|
$result = $conn->query($sql);
|
||||||
|
if ($result) {
|
||||||
|
foreach ($result as $row) {
|
||||||
|
$items[] = $row[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif (is_resource($conn)) {
|
||||||
|
$result = sqlsrv_query($conn, $sql);
|
||||||
|
if ($result) {
|
||||||
|
while ($row = sqlsrv_fetch_array($result, SQLSRV_FETCH_NUMERIC)) {
|
||||||
|
$items[] = $row[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $items;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function requireDatabaseFunctions($databaseConfig)
|
||||||
|
{
|
||||||
|
$data = DatabaseAdapterRegistry::get_adapter($databaseConfig['type']);
|
||||||
|
return !empty($data['supported']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function requireDatabaseServer($databaseConfig)
|
||||||
|
{
|
||||||
|
$conn = $this->createConnection($databaseConfig, $error);
|
||||||
|
$success = !empty($conn);
|
||||||
|
|
||||||
return array(
|
return array(
|
||||||
'success' => $success,
|
'success' => $success,
|
||||||
'error' => $error
|
'error' => $error
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function requireDatabaseConnection($databaseConfig)
|
||||||
* Ensure a database connection is possible using credentials provided.
|
{
|
||||||
* @param array $databaseConfig Associative array of db configuration, e.g. "server", "username" etc
|
$conn = $this->createConnection($databaseConfig, $error);
|
||||||
* @return array Result - e.g. array('success' => true, 'error' => 'details of error')
|
$success = !empty($conn);
|
||||||
*/
|
|
||||||
public function requireDatabaseConnection($databaseConfig) {
|
|
||||||
$success = false;
|
|
||||||
$error = '';
|
|
||||||
|
|
||||||
if(function_exists('mssql_connect')) {
|
|
||||||
$conn = @mssql_connect($databaseConfig['server'], $databaseConfig['username'], $databaseConfig['password'], true);
|
|
||||||
} else {
|
|
||||||
$conn = @sqlsrv_connect($databaseConfig['server'], array(
|
|
||||||
'UID' => $databaseConfig['username'],
|
|
||||||
'PWD' => $databaseConfig['password']
|
|
||||||
));
|
|
||||||
|
|
||||||
$errors = sqlsrv_errors();
|
|
||||||
if($errors) {
|
|
||||||
$error .= "\n";
|
|
||||||
foreach($errors as $detail) {
|
|
||||||
$error .= "\n" . @$detail['message'] . "\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if($conn) {
|
|
||||||
$success = true;
|
|
||||||
} else {
|
|
||||||
$success = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return array(
|
return array(
|
||||||
'success' => $success,
|
'success' => $success,
|
||||||
@ -98,37 +182,22 @@ class MSSQLDatabaseConfigurationHelper implements DatabaseConfigurationHelper {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getDatabaseVersion($databaseConfig) {
|
public function getDatabaseVersion($databaseConfig)
|
||||||
$version = 0;
|
{
|
||||||
|
$conn = $this->createConnection($databaseConfig, $error);
|
||||||
// Get the version using SERVERPROPERTY() function
|
$result = $this->query($conn, "SELECT CONVERT(char(15), SERVERPROPERTY('ProductVersion'))");
|
||||||
if(function_exists('mssql_connect')) {
|
return empty($result) ? 0 : reset($result);
|
||||||
$conn = @mssql_connect($databaseConfig['server'], $databaseConfig['username'], $databaseConfig['password'], true);
|
|
||||||
$result = @mssql_query("SELECT CONVERT(char(15), SERVERPROPERTY('ProductVersion'))", $conn);
|
|
||||||
$row = @mssql_fetch_array($result);
|
|
||||||
} else {
|
|
||||||
$conn = @sqlsrv_connect($databaseConfig['server'], array(
|
|
||||||
'UID' => $databaseConfig['username'],
|
|
||||||
'PWD' => $databaseConfig['password']
|
|
||||||
));
|
|
||||||
$result = @sqlsrv_query($conn, "SELECT CONVERT(char(15), SERVERPROPERTY('ProductVersion'))");
|
|
||||||
$row = @sqlsrv_fetch_array($result);
|
|
||||||
}
|
|
||||||
|
|
||||||
if($row && isset($row[0])) {
|
|
||||||
$version = trim($row[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $version;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure that the SQL Server version is at least 10.00.2531 (SQL Server 2008 SP1).
|
* Ensure that the SQL Server version is at least 10.00.2531 (SQL Server 2008 SP1).
|
||||||
|
*
|
||||||
* @see http://www.sqlteam.com/article/sql-server-versions
|
* @see http://www.sqlteam.com/article/sql-server-versions
|
||||||
* @param array $databaseConfig Associative array of db configuration, e.g. "server", "username" etc
|
* @param array $databaseConfig Associative array of db configuration, e.g. "server", "username" etc
|
||||||
* @return array Result - e.g. array('success' => true, 'error' => 'details of error')
|
* @return array Result - e.g. array('success' => true, 'error' => 'details of error')
|
||||||
*/
|
*/
|
||||||
public function requireDatabaseVersion($databaseConfig) {
|
public function requireDatabaseVersion($databaseConfig)
|
||||||
|
{
|
||||||
$success = false;
|
$success = false;
|
||||||
$error = '';
|
$error = '';
|
||||||
$version = $this->getDatabaseVersion($databaseConfig);
|
$version = $this->getDatabaseVersion($databaseConfig);
|
||||||
@ -148,37 +217,26 @@ class MSSQLDatabaseConfigurationHelper implements DatabaseConfigurationHelper {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function requireDatabaseOrCreatePermissions($databaseConfig)
|
||||||
* Ensure that the database connection is able to use an existing database,
|
{
|
||||||
* or be able to create one if it doesn't exist.
|
$conn = $this->createConnection($databaseConfig, $error);
|
||||||
*
|
/** @skipUpgrade */
|
||||||
* @param array $databaseConfig Associative array of db configuration, e.g. "server", "username" etc
|
if (empty($conn)) {
|
||||||
* @return array Result - e.g. array('success' => true, 'alreadyExists' => 'true')
|
|
||||||
*/
|
|
||||||
public function requireDatabaseOrCreatePermissions($databaseConfig) {
|
|
||||||
$success = false;
|
$success = false;
|
||||||
$alreadyExists = false;
|
$alreadyExists = false;
|
||||||
|
} elseif ($databaseConfig['type'] == 'MSSQLAzureDatabase') {
|
||||||
$check = $this->requireDatabaseConnection($databaseConfig);
|
// Don't bother with DB selection for azure, as it's not supported
|
||||||
$conn = $check['connection'];
|
|
||||||
|
|
||||||
if(function_exists('mssql_select_db')) {
|
|
||||||
$exists = @mssql_select_db($databaseConfig['database'], $conn);
|
|
||||||
} else {
|
|
||||||
$exists = @sqlsrv_query($conn, "USE \"$databaseConfig[database]\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
if($exists) {
|
|
||||||
$success = true;
|
$success = true;
|
||||||
$alreadyExists = true;
|
$alreadyExists = true;
|
||||||
} else {
|
} else {
|
||||||
if(function_exists('mssql_query') && mssql_query("CREATE DATABASE testing123", $conn)) {
|
// does this database exist already?
|
||||||
mssql_query("DROP DATABASE testing123", $conn);
|
$list = $this->query($conn, 'SELECT NAME FROM sys.sysdatabases');
|
||||||
$success = true;
|
if (in_array($databaseConfig['database'], $list)) {
|
||||||
$alreadyExists = false;
|
|
||||||
} elseif(function_exists('sqlsrv_query') && @sqlsrv_query($conn, "CREATE DATABASE testing123")) {
|
|
||||||
sqlsrv_query($conn, "DROP DATABASE testing123");
|
|
||||||
$success = true;
|
$success = true;
|
||||||
|
$alreadyExists = true;
|
||||||
|
} else {
|
||||||
|
$permissions = $this->query($conn, "select COUNT(*) from sys.fn_my_permissions('','') where permission_name like 'CREATE ANY DATABASE' or permission_name like 'CREATE DATABASE';");
|
||||||
|
$success = $permissions[0] > 0;
|
||||||
$alreadyExists = false;
|
$alreadyExists = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -189,4 +247,22 @@ class MSSQLDatabaseConfigurationHelper implements DatabaseConfigurationHelper {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function requireDatabaseAlterPermissions($databaseConfig)
|
||||||
|
{
|
||||||
|
$success = false;
|
||||||
|
$conn = $this->createConnection($databaseConfig, $error);
|
||||||
|
if (!empty($conn)) {
|
||||||
|
if (!$this->isAzure($databaseConfig)) {
|
||||||
|
// Make sure to select the current database when checking permission against this database
|
||||||
|
$this->query($conn, "USE \"{$databaseConfig['database']}\"");
|
||||||
|
}
|
||||||
|
$permissions = $this->query($conn, "select COUNT(*) from sys.fn_my_permissions(NULL,'DATABASE') WHERE permission_name like 'create table';");
|
||||||
|
$success = $permissions[0] > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'success' => $success,
|
||||||
|
'applies' => true
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
124
code/MSSQLQueryBuilder.php
Normal file
124
code/MSSQLQueryBuilder.php
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\MSSQL;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use SilverStripe\ORM\Queries\SQLSelect;
|
||||||
|
use SilverStripe\ORM\Connect\DBQueryBuilder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a SQL query string from a SQLExpression object
|
||||||
|
*/
|
||||||
|
class MSSQLQueryBuilder extends DBQueryBuilder
|
||||||
|
{
|
||||||
|
|
||||||
|
protected function buildSelectQuery(SQLSelect $query, array &$parameters)
|
||||||
|
{
|
||||||
|
list($limit, $offset) = $this->parseLimit($query);
|
||||||
|
|
||||||
|
// If not using ofset then query generation is quite straightforward
|
||||||
|
if (empty($offset)) {
|
||||||
|
$sql = parent::buildSelectQuery($query, $parameters);
|
||||||
|
// Inject limit into SELECT fragment
|
||||||
|
if (!empty($limit)) {
|
||||||
|
$sql = preg_replace('/^(SELECT (DISTINCT)?)/i', '${1} TOP '.$limit, $sql);
|
||||||
|
}
|
||||||
|
return $sql;
|
||||||
|
}
|
||||||
|
|
||||||
|
// When using offset we must use a subselect
|
||||||
|
// @see http://stackoverflow.com/questions/2135418/equivalent-of-limit-and-offset-for-sql-server
|
||||||
|
$orderby = $query->getOrderBy();
|
||||||
|
|
||||||
|
// workaround for subselect not working with alias functions
|
||||||
|
// just use the function directly in the order by instead of the alias
|
||||||
|
$selects = $query->getSelect();
|
||||||
|
foreach ($orderby as $field => $dir) {
|
||||||
|
if (preg_match('/_SortColumn/', $field)) {
|
||||||
|
unset($orderby[$field]);
|
||||||
|
$orderby[$selects[str_replace('"', '', $field)]] = $dir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create order expression, using the first column if none explicitly specified
|
||||||
|
if ($orderby) {
|
||||||
|
// Simple implementation of buildOrderByFragment
|
||||||
|
$statements = array();
|
||||||
|
foreach ($orderby as $clause => $dir) {
|
||||||
|
$statements[] = trim("$clause $dir");
|
||||||
|
}
|
||||||
|
$orderByClause = "ORDER BY " . implode(', ', $statements);
|
||||||
|
} else {
|
||||||
|
$selects = $query->getSelect();
|
||||||
|
$firstCol = reset($selects);
|
||||||
|
$orderByClause = "ORDER BY $firstCol";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build main query SQL
|
||||||
|
$sql = parent::buildSelectQuery($query, $parameters);
|
||||||
|
|
||||||
|
// Inject row number into selection
|
||||||
|
$sql = preg_replace('/^(SELECT (DISTINCT)?)/i', '${1} ROW_NUMBER() OVER ('.$orderByClause.') AS Number, ', $sql);
|
||||||
|
|
||||||
|
// Sub-query this SQL
|
||||||
|
if (empty($limit)) {
|
||||||
|
$limitCondition = "Number > ?";
|
||||||
|
$parameters[] = $offset;
|
||||||
|
} else {
|
||||||
|
$limitCondition = "Number BETWEEN ? AND ?";
|
||||||
|
$parameters[] = $offset + 1;
|
||||||
|
$parameters[] = $offset + $limit;
|
||||||
|
}
|
||||||
|
return "SELECT * FROM ($sql) AS Numbered WHERE $limitCondition ORDER BY Number";
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildLimitFragment(SQLSelect $query, array &$parameters)
|
||||||
|
{
|
||||||
|
// Limit is handled at the buildSelectQuery level
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildOrderByFragment(SQLSelect $query, array &$parameters)
|
||||||
|
{
|
||||||
|
// If doing a limit/offset at the same time then don't build the orde by fragment here
|
||||||
|
list($offset, $limit) = $this->parseLimit($query);
|
||||||
|
if (empty($offset) || empty($limit)) {
|
||||||
|
return parent::buildOrderByFragment($query, $parameters);
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the limit and offset from the limit clause
|
||||||
|
*
|
||||||
|
* @param SQLSelect $query
|
||||||
|
* @return array Two item array with $limit and $offset as values
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
protected function parseLimit(SQLSelect $query)
|
||||||
|
{
|
||||||
|
$limit = '';
|
||||||
|
$offset = '0';
|
||||||
|
if (is_array($query->getLimit())) {
|
||||||
|
$limitArr = $query->getLimit();
|
||||||
|
if (isset($limitArr['limit'])) {
|
||||||
|
$limit = $limitArr['limit'];
|
||||||
|
}
|
||||||
|
if (isset($limitArr['start'])) {
|
||||||
|
$offset = $limitArr['start'];
|
||||||
|
}
|
||||||
|
} elseif (preg_match('/^([0-9]+) offset ([0-9]+)$/i', trim($query->getLimit()), $matches)) {
|
||||||
|
$limit = $matches[1];
|
||||||
|
$offset = $matches[2];
|
||||||
|
} else {
|
||||||
|
//could be a comma delimited string
|
||||||
|
$bits = explode(',', $query->getLimit());
|
||||||
|
if (sizeof($bits) > 1) {
|
||||||
|
list($offset, $limit) = $bits;
|
||||||
|
} else {
|
||||||
|
$limit = $bits[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return array($limit, $offset);
|
||||||
|
}
|
||||||
|
}
|
964
code/MSSQLSchemaManager.php
Normal file
964
code/MSSQLSchemaManager.php
Normal file
@ -0,0 +1,964 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\MSSQL;
|
||||||
|
|
||||||
|
use SilverStripe\ORM\Connect\DBSchemaManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents and handles all schema management for a MS SQL database
|
||||||
|
*/
|
||||||
|
class MSSQLSchemaManager extends DBSchemaManager
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores per-request cached constraint checks that come from the database.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected static $cached_checks = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the internal MS SQL Server index name given the silverstripe table and index name
|
||||||
|
*
|
||||||
|
* @param string $tableName
|
||||||
|
* @param string $indexName
|
||||||
|
* @param string $prefix The optional prefix for the index. Defaults to "ix" for indexes.
|
||||||
|
* @return string The name of the index
|
||||||
|
*/
|
||||||
|
public function buildMSSQLIndexName($tableName, $indexName, $prefix = 'ix')
|
||||||
|
{
|
||||||
|
|
||||||
|
// Cleanup names of namespaced tables
|
||||||
|
$tableName = str_replace('\\', '_', $tableName);
|
||||||
|
$indexName = str_replace('\\', '_', $indexName);
|
||||||
|
|
||||||
|
return "{$prefix}_{$tableName}_{$indexName}";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This will set up the full text search capabilities.
|
||||||
|
*
|
||||||
|
* @param string $name Name of full text catalog to use
|
||||||
|
*/
|
||||||
|
public function createFullTextCatalog($name = 'ftCatalog')
|
||||||
|
{
|
||||||
|
$result = $this->fullTextCatalogExists();
|
||||||
|
if (!$result) {
|
||||||
|
$this->query("CREATE FULLTEXT CATALOG \"$name\" AS DEFAULT;");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that a fulltext catalog has been created yet.
|
||||||
|
*
|
||||||
|
* @param string $name Name of full text catalog to use
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
public function fullTextCatalogExists($name = 'ftCatalog')
|
||||||
|
{
|
||||||
|
return (bool) $this->preparedQuery(
|
||||||
|
"SELECT name FROM sys.fulltext_catalogs WHERE name = ?;",
|
||||||
|
array($name)
|
||||||
|
)->value();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sleep until the catalog has been fully rebuilt. This is a busy wait designed for situations
|
||||||
|
* when you need to be sure the index is up to date - for example in unit tests.
|
||||||
|
*
|
||||||
|
* TODO: move this to Database class? Can we assume this will be useful for all databases?
|
||||||
|
* Also see the wrapper functions "waitUntilIndexingFinished" in SearchFormTest and TranslatableSearchFormTest
|
||||||
|
*
|
||||||
|
* @param int $maxWaitingTime Time in seconds to wait for the database.
|
||||||
|
*/
|
||||||
|
public function waitUntilIndexingFinished($maxWaitingTime = 15)
|
||||||
|
{
|
||||||
|
if (!$this->database->fullTextEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->query("EXEC sp_fulltext_catalog 'ftCatalog', 'Rebuild';");
|
||||||
|
|
||||||
|
// Busy wait until it's done updating, but no longer than 15 seconds.
|
||||||
|
$start = time();
|
||||||
|
while (time() - $start < $maxWaitingTime) {
|
||||||
|
$status = $this->query("EXEC sp_help_fulltext_catalogs 'ftCatalog';")->first();
|
||||||
|
|
||||||
|
if (isset($status['STATUS']) && $status['STATUS'] == 0) {
|
||||||
|
// Idle!
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
sleep(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a fulltext index exists on a particular table name.
|
||||||
|
*
|
||||||
|
* @param string $tableName
|
||||||
|
* @return boolean TRUE index exists | FALSE index does not exist | NULL no support
|
||||||
|
*/
|
||||||
|
public function fulltextIndexExists($tableName)
|
||||||
|
{
|
||||||
|
// Special case for no full text index support
|
||||||
|
if (!$this->database->fullTextEnabled()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (bool) $this->preparedQuery("
|
||||||
|
SELECT 1 FROM sys.fulltext_indexes i
|
||||||
|
JOIN sys.objects o ON i.object_id = o.object_id
|
||||||
|
WHERE o.name = ?",
|
||||||
|
array($tableName)
|
||||||
|
)->value();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MSSQL stores the primary key column with an internal identifier,
|
||||||
|
* so a lookup needs to be done to determine it.
|
||||||
|
*
|
||||||
|
* @param string $tableName Name of table with primary key column "ID"
|
||||||
|
* @return string Internal identifier for primary key
|
||||||
|
*/
|
||||||
|
public function getPrimaryKey($tableName)
|
||||||
|
{
|
||||||
|
$indexes = $this->query("EXEC sp_helpindex '$tableName';");
|
||||||
|
$indexName = '';
|
||||||
|
foreach ($indexes as $index) {
|
||||||
|
if ($index['index_keys'] == 'ID') {
|
||||||
|
$indexName = $index['index_name'];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $indexName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the identity column of a table
|
||||||
|
*
|
||||||
|
* @param string $tableName
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function getIdentityColumn($tableName)
|
||||||
|
{
|
||||||
|
return $this->preparedQuery("
|
||||||
|
SELECT
|
||||||
|
TABLE_NAME + '.' + COLUMN_NAME,
|
||||||
|
TABLE_NAME
|
||||||
|
FROM
|
||||||
|
INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE
|
||||||
|
TABLE_SCHEMA = ? AND
|
||||||
|
COLUMNPROPERTY(object_id(TABLE_NAME), COLUMN_NAME, 'IsIdentity') = 1 AND
|
||||||
|
TABLE_NAME = ?
|
||||||
|
", array('dbo', $tableName))->value();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createDatabase($name)
|
||||||
|
{
|
||||||
|
$this->query("CREATE DATABASE \"$name\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dropDatabase($name)
|
||||||
|
{
|
||||||
|
$this->query("DROP DATABASE \"$name\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function databaseExists($name)
|
||||||
|
{
|
||||||
|
$databases = $this->databaseList();
|
||||||
|
foreach ($databases as $dbname) {
|
||||||
|
if ($dbname == $name) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function databaseList()
|
||||||
|
{
|
||||||
|
return $this->query('SELECT NAME FROM sys.sysdatabases')->column();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new table.
|
||||||
|
* @param string $tableName The name of the table
|
||||||
|
* @param array $fields A map of field names to field types
|
||||||
|
* @param array $indexes A map of indexes
|
||||||
|
* @param array $options An map of additional options. The available keys are as follows:
|
||||||
|
* - 'MSSQLDatabase'/'MySQLDatabase'/'PostgreSQLDatabase' - database-specific options such as "engine" for MySQL.
|
||||||
|
* - 'temporary' - If true, then a temporary table will be created
|
||||||
|
* @param array $advancedOptions
|
||||||
|
* @return string The table name generated. This may be different from the table name, for example with temporary tables.
|
||||||
|
*/
|
||||||
|
public function createTable($tableName, $fields = null, $indexes = null, $options = null, $advancedOptions = null)
|
||||||
|
{
|
||||||
|
$fieldSchemas = $indexSchemas = "";
|
||||||
|
if ($fields) {
|
||||||
|
foreach ($fields as $k => $v) {
|
||||||
|
$fieldSchemas .= "\"$k\" $v,\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temporary tables start with "#" in MSSQL-land
|
||||||
|
if (!empty($options['temporary'])) {
|
||||||
|
// Randomize the temp table name to avoid conflicts in the tempdb table which derived databases share
|
||||||
|
$tableName = "#$tableName" . '-' . rand(1000000, 9999999);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->query("CREATE TABLE \"$tableName\" (
|
||||||
|
$fieldSchemas
|
||||||
|
primary key (\"ID\")
|
||||||
|
);");
|
||||||
|
|
||||||
|
//we need to generate indexes like this: CREATE INDEX IX_vault_to_export ON vault (to_export);
|
||||||
|
//This needs to be done AFTER the table creation, so we can set up the fulltext indexes correctly
|
||||||
|
if ($indexes) {
|
||||||
|
foreach ($indexes as $k => $v) {
|
||||||
|
$indexSchemas .= $this->getIndexSqlDefinition($tableName, $k, $v) . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($indexSchemas) {
|
||||||
|
$this->query($indexSchemas);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tableName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alter a table's schema.
|
||||||
|
* @param string $tableName The name of the table to alter
|
||||||
|
* @param array $newFields New fields, a map of field name => field schema
|
||||||
|
* @param array $newIndexes New indexes, a map of index name => index type
|
||||||
|
* @param array $alteredFields Updated fields, a map of field name => field schema
|
||||||
|
* @param array $alteredIndexes Updated indexes, a map of index name => index type
|
||||||
|
* @param array $alteredOptions
|
||||||
|
* @param array $advancedOptions
|
||||||
|
*/
|
||||||
|
public function alterTable($tableName, $newFields = null, $newIndexes = null, $alteredFields = null, $alteredIndexes = null, $alteredOptions=null, $advancedOptions=null)
|
||||||
|
{
|
||||||
|
$alterList = array();
|
||||||
|
|
||||||
|
// drop any fulltext indexes that exist on the table before altering the structure
|
||||||
|
if ($this->fulltextIndexExists($tableName)) {
|
||||||
|
$alterList[] = "\nDROP FULLTEXT INDEX ON \"$tableName\";";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($newFields) {
|
||||||
|
foreach ($newFields as $k => $v) {
|
||||||
|
$alterList[] = "ALTER TABLE \"$tableName\" ADD \"$k\" $v";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($alteredFields) {
|
||||||
|
foreach ($alteredFields as $k => $v) {
|
||||||
|
$alterList[] = $this->alterTableAlterColumn($tableName, $k, $v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($alteredIndexes) {
|
||||||
|
foreach ($alteredIndexes as $k => $v) {
|
||||||
|
$alterList[] = $this->getIndexSqlDefinition($tableName, $k, $v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($newIndexes) {
|
||||||
|
foreach ($newIndexes as $k => $v) {
|
||||||
|
$alterList[] = $this->getIndexSqlDefinition($tableName, $k, $v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($alterList) {
|
||||||
|
foreach ($alterList as $alteration) {
|
||||||
|
if ($alteration != '') {
|
||||||
|
$this->query($alteration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given the table and column name, retrieve the constraint name for that column
|
||||||
|
* in the table.
|
||||||
|
*
|
||||||
|
* @param string $tableName Table name column resides in
|
||||||
|
* @param string $columnName Column name the constraint is for
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function getConstraintName($tableName, $columnName)
|
||||||
|
{
|
||||||
|
return $this->preparedQuery("
|
||||||
|
SELECT CONSTRAINT_NAME
|
||||||
|
FROM INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE
|
||||||
|
WHERE TABLE_NAME = ? AND COLUMN_NAME = ?",
|
||||||
|
array($tableName, $columnName)
|
||||||
|
)->value();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a table and column name, return a check constraint clause for that column in
|
||||||
|
* the table.
|
||||||
|
*
|
||||||
|
* This is an expensive query, so it is cached per-request and stored by table. The initial
|
||||||
|
* call for a table that has not been cached will query all columns and store that
|
||||||
|
* so subsequent calls are fast.
|
||||||
|
*
|
||||||
|
* @param string $tableName Table name column resides in
|
||||||
|
* @param string $columnName Column name the constraint is for
|
||||||
|
* @return string The check string
|
||||||
|
*/
|
||||||
|
public function getConstraintCheckClause($tableName, $columnName)
|
||||||
|
{
|
||||||
|
// Check already processed table columns
|
||||||
|
if (isset(self::$cached_checks[$tableName])) {
|
||||||
|
if (!isset(self::$cached_checks[$tableName][$columnName])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return self::$cached_checks[$tableName][$columnName];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regenerate cehcks for this table
|
||||||
|
$checks = array();
|
||||||
|
foreach ($this->preparedQuery("
|
||||||
|
SELECT CAST(CHECK_CLAUSE AS TEXT) AS CHECK_CLAUSE, COLUMN_NAME
|
||||||
|
FROM INFORMATION_SCHEMA.CHECK_CONSTRAINTS AS CC
|
||||||
|
INNER JOIN INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE AS CCU ON CCU.CONSTRAINT_NAME = CC.CONSTRAINT_NAME
|
||||||
|
WHERE TABLE_NAME = ?",
|
||||||
|
array($tableName)
|
||||||
|
) as $record) {
|
||||||
|
$checks[$record['COLUMN_NAME']] = $record['CHECK_CLAUSE'];
|
||||||
|
}
|
||||||
|
self::$cached_checks[$tableName] = $checks;
|
||||||
|
|
||||||
|
// Return via cached records
|
||||||
|
return $this->getConstraintCheckClause($tableName, $columnName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the name of the default constraint applied to $tableName.$colName.
|
||||||
|
* Will return null if no such constraint exists
|
||||||
|
*
|
||||||
|
* @param string $tableName Name of the table
|
||||||
|
* @param string $colName Name of the column
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
protected function defaultConstraintName($tableName, $colName)
|
||||||
|
{
|
||||||
|
return $this->preparedQuery("
|
||||||
|
SELECT s.name --default name
|
||||||
|
FROM sys.sysobjects s
|
||||||
|
join sys.syscolumns c ON s.parent_obj = c.id
|
||||||
|
WHERE s.xtype = 'd'
|
||||||
|
and c.cdefault = s.id
|
||||||
|
and parent_obj = OBJECT_ID(?)
|
||||||
|
and c.name = ?",
|
||||||
|
array($tableName, $colName)
|
||||||
|
)->value();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get enum values from a constraint check clause.
|
||||||
|
*
|
||||||
|
* @param string $clause Check clause to parse values from
|
||||||
|
* @return array Enum values
|
||||||
|
*/
|
||||||
|
protected function enumValuesFromCheckClause($clause)
|
||||||
|
{
|
||||||
|
$segments = preg_split('/ +OR *\[/i', $clause);
|
||||||
|
$constraints = array();
|
||||||
|
foreach ($segments as $segment) {
|
||||||
|
$bits = preg_split('/ *= */', $segment);
|
||||||
|
for ($i = 1; $i < sizeof($bits); $i += 2) {
|
||||||
|
array_unshift($constraints, substr(rtrim($bits[$i], ')'), 1, -1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $constraints;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Creates an ALTER expression for a column in MS SQL
|
||||||
|
*
|
||||||
|
* @param string $tableName Name of the table to be altered
|
||||||
|
* @param string $colName Name of the column to be altered
|
||||||
|
* @param string $colSpec String which contains conditions for a column
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function alterTableAlterColumn($tableName, $colName, $colSpec)
|
||||||
|
{
|
||||||
|
|
||||||
|
// First, we split the column specifications into parts
|
||||||
|
// TODO: this returns an empty array for the following string: int(11) not null auto_increment
|
||||||
|
// on second thoughts, why is an auto_increment field being passed through?
|
||||||
|
$pattern = '/^(?<definition>[\w()]+)\s?(?<null>(?:not\s)?null)?\s?(?<default>default\s[\w\']+)?\s?(?<check>check\s?[\w()\'",\s]+)?$/i';
|
||||||
|
$matches = array();
|
||||||
|
preg_match($pattern, $colSpec, $matches);
|
||||||
|
|
||||||
|
// drop the index if it exists
|
||||||
|
$alterQueries = array();
|
||||||
|
|
||||||
|
// drop *ALL* indexes on a table before proceeding
|
||||||
|
// this won't drop primary keys, though
|
||||||
|
$indexes = $this->indexNames($tableName);
|
||||||
|
$indexes = array_filter($indexes);
|
||||||
|
|
||||||
|
foreach ($indexes as $indexName) {
|
||||||
|
$alterQueries[] = "IF EXISTS (SELECT name FROM sys.indexes WHERE name = '$indexName' AND object_id = object_id(SCHEMA_NAME() + '.$tableName')) DROP INDEX \"$indexName\" ON \"$tableName\";";
|
||||||
|
}
|
||||||
|
|
||||||
|
$prefix = "ALTER TABLE \"$tableName\" ";
|
||||||
|
|
||||||
|
// Remove the old default prior to adjusting the column.
|
||||||
|
if ($defaultConstraintName = $this->defaultConstraintName($tableName, $colName)) {
|
||||||
|
$alterQueries[] = "$prefix DROP CONSTRAINT \"$defaultConstraintName\";";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($matches['definition'])) {
|
||||||
|
//We will prevent any changes being made to the ID column. Primary key indexes will have a fit if we do anything here.
|
||||||
|
if ($colName != 'ID') {
|
||||||
|
|
||||||
|
// SET null / not null
|
||||||
|
$nullFragment = empty($matches['null']) ? '' : " {$matches['null']}";
|
||||||
|
$alterQueries[] = "$prefix ALTER COLUMN \"$colName\" {$matches['definition']}$nullFragment;";
|
||||||
|
|
||||||
|
// Add a default back
|
||||||
|
if (!empty($matches['default'])) {
|
||||||
|
$alterQueries[] = "$prefix ADD {$matches['default']} FOR \"$colName\";";
|
||||||
|
}
|
||||||
|
|
||||||
|
// SET check constraint (The constraint HAS to be dropped)
|
||||||
|
if (!empty($matches['check'])) {
|
||||||
|
$constraint = $this->getConstraintName($tableName, $colName);
|
||||||
|
if ($constraint) {
|
||||||
|
$alterQueries[] = "$prefix DROP CONSTRAINT {$constraint};";
|
||||||
|
}
|
||||||
|
|
||||||
|
//NOTE: 'with nocheck' seems to solve a few problems I've been having for modifying existing tables.
|
||||||
|
$alterQueries[] = "$prefix WITH NOCHECK ADD CONSTRAINT \"{$tableName}_{$colName}_check\" {$matches['check']};";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode("\n", $alterQueries);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function renameTable($oldTableName, $newTableName)
|
||||||
|
{
|
||||||
|
$this->query("EXEC sp_rename \"$oldTableName\", \"$newTableName\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks a table's integrity and repairs it if necessary.
|
||||||
|
* NOTE: MSSQL does not appear to support any vacuum or optimise commands
|
||||||
|
*
|
||||||
|
* @var string $tableName The name of the table.
|
||||||
|
* @return boolean Return true if the table has integrity after the method is complete.
|
||||||
|
*/
|
||||||
|
public function checkAndRepairTable($tableName)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createField($tableName, $fieldName, $fieldSpec)
|
||||||
|
{
|
||||||
|
$this->query("ALTER TABLE \"$tableName\" ADD \"$fieldName\" $fieldSpec");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the database type of the given field.
|
||||||
|
* @param string $tableName The name of the tbale the field is in.
|
||||||
|
* @param string $fieldName The name of the field to change.
|
||||||
|
* @param string $fieldSpec The new field specification
|
||||||
|
*/
|
||||||
|
public function alterField($tableName, $fieldName, $fieldSpec)
|
||||||
|
{
|
||||||
|
$this->query("ALTER TABLE \"$tableName\" CHANGE \"$fieldName\" \"$fieldName\" $fieldSpec");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function renameField($tableName, $oldName, $newName)
|
||||||
|
{
|
||||||
|
$this->query("EXEC sp_rename @objname = '$tableName.$oldName', @newname = '$newName', @objtype = 'COLUMN'");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fieldList($table)
|
||||||
|
{
|
||||||
|
//This gets us more information than we need, but I've included it all for the moment....
|
||||||
|
$fieldRecords = $this->preparedQuery("SELECT ordinal_position, column_name, data_type, column_default,
|
||||||
|
is_nullable, character_maximum_length, numeric_precision, numeric_scale, collation_name
|
||||||
|
FROM information_schema.columns WHERE table_name = ?
|
||||||
|
ORDER BY ordinal_position;",
|
||||||
|
array($table)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cache the records from the query - otherwise a lack of multiple active result sets
|
||||||
|
// will cause subsequent queries to fail in this method
|
||||||
|
$fields = array();
|
||||||
|
$output = array();
|
||||||
|
foreach ($fieldRecords as $record) {
|
||||||
|
$fields[] = $record;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($fields as $field) {
|
||||||
|
// Update the data_type field to be a complete column definition string for use by
|
||||||
|
// SS_Database::requireField()
|
||||||
|
switch ($field['data_type']) {
|
||||||
|
case 'int':
|
||||||
|
case 'bigint':
|
||||||
|
case 'numeric':
|
||||||
|
case 'float':
|
||||||
|
case 'bit':
|
||||||
|
if ($field['data_type'] != 'bigint' && $field['data_type'] != 'int' && $sizeSuffix = $field['numeric_precision']) {
|
||||||
|
$field['data_type'] .= "($sizeSuffix)";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($field['is_nullable'] == 'YES') {
|
||||||
|
$field['data_type'] .= ' null';
|
||||||
|
} else {
|
||||||
|
$field['data_type'] .= ' not null';
|
||||||
|
}
|
||||||
|
if ($field['column_default']) {
|
||||||
|
$default=substr($field['column_default'], 2, -2);
|
||||||
|
$field['data_type'] .= " default $default";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'decimal':
|
||||||
|
if ($field['numeric_precision']) {
|
||||||
|
$sizeSuffix = $field['numeric_precision'] . ',' . $field['numeric_scale'];
|
||||||
|
$field['data_type'] .= "($sizeSuffix)";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($field['is_nullable'] == 'YES') {
|
||||||
|
$field['data_type'] .= ' null';
|
||||||
|
} else {
|
||||||
|
$field['data_type'] .= ' not null';
|
||||||
|
}
|
||||||
|
if ($field['column_default']) {
|
||||||
|
$default=substr($field['column_default'], 2, -2);
|
||||||
|
$field['data_type'] .= " default $default";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'nvarchar':
|
||||||
|
case 'varchar':
|
||||||
|
//Check to see if there's a constraint attached to this column:
|
||||||
|
$clause = $this->getConstraintCheckClause($table, $field['column_name']);
|
||||||
|
if ($clause) {
|
||||||
|
$constraints = $this->enumValuesFromCheckClause($clause);
|
||||||
|
$default=substr($field['column_default'], 2, -2);
|
||||||
|
$field['data_type'] = $this->enum(array(
|
||||||
|
'default' => $default,
|
||||||
|
'name' => $field['column_name'],
|
||||||
|
'enums' => $constraints,
|
||||||
|
'table' => $table
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
$sizeSuffix = $field['character_maximum_length'];
|
||||||
|
if ($sizeSuffix == '-1') {
|
||||||
|
$sizeSuffix = 'max';
|
||||||
|
}
|
||||||
|
if ($sizeSuffix) {
|
||||||
|
$field['data_type'] .= "($sizeSuffix)";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($field['is_nullable'] == 'YES') {
|
||||||
|
$field['data_type'] .= ' null';
|
||||||
|
} else {
|
||||||
|
$field['data_type'] .= ' not null';
|
||||||
|
}
|
||||||
|
if ($field['column_default']) {
|
||||||
|
$default=substr($field['column_default'], 2, -2);
|
||||||
|
$field['data_type'] .= " default '$default'";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$output[$field['column_name']] = $field;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an index on a table.
|
||||||
|
* @param string $tableName The name of the table.
|
||||||
|
* @param string $indexName The name of the index.
|
||||||
|
* @param string $indexSpec The specification of the index, see SS_Database::requireIndex() for more details.
|
||||||
|
*/
|
||||||
|
public function createIndex($tableName, $indexName, $indexSpec)
|
||||||
|
{
|
||||||
|
$this->query($this->getIndexSqlDefinition($tableName, $indexName, $indexSpec));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return SQL for dropping and recreating an index
|
||||||
|
*
|
||||||
|
* @param string $tableName Name of table to create this index against
|
||||||
|
* @param string $indexName Name of this index
|
||||||
|
* @param array|string $indexSpec Index specification, either as a raw string
|
||||||
|
* or parsed array form
|
||||||
|
* @return string The SQL required to generate this index
|
||||||
|
*/
|
||||||
|
protected function getIndexSqlDefinition($tableName, $indexName, $indexSpec)
|
||||||
|
{
|
||||||
|
|
||||||
|
// Determine index name
|
||||||
|
$index = $this->buildMSSQLIndexName($tableName, $indexName);
|
||||||
|
|
||||||
|
// Consolidate/Cleanup spec into array format
|
||||||
|
$indexSpec = $this->parseIndexSpec($indexName, $indexSpec);
|
||||||
|
|
||||||
|
$drop = "IF EXISTS (SELECT name FROM sys.indexes WHERE name = '$index' AND object_id = object_id(SCHEMA_NAME() + '.$tableName')) DROP INDEX $index ON \"$tableName\";";
|
||||||
|
|
||||||
|
// create a type-specific index
|
||||||
|
if ($indexSpec['type'] == 'fulltext') {
|
||||||
|
if(!$this->database->fullTextEnabled()) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
// enable fulltext on this table
|
||||||
|
$this->createFullTextCatalog();
|
||||||
|
$primary_key = $this->getPrimaryKey($tableName);
|
||||||
|
|
||||||
|
if ($primary_key) {
|
||||||
|
return "$drop CREATE FULLTEXT INDEX ON \"$tableName\" ({$indexSpec['value']})"
|
||||||
|
. "KEY INDEX $primary_key WITH CHANGE_TRACKING AUTO;";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($indexSpec['type'] == 'unique') {
|
||||||
|
return "$drop CREATE UNIQUE INDEX $index ON \"$tableName\" ({$indexSpec['value']});";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "$drop CREATE INDEX $index ON \"$tableName\" ({$indexSpec['value']});";
|
||||||
|
}
|
||||||
|
|
||||||
|
public function alterIndex($tableName, $indexName, $indexSpec)
|
||||||
|
{
|
||||||
|
$this->createIndex($tableName, $indexName, $indexSpec);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the list of indexes in a table.
|
||||||
|
* @param string $table The table name.
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function indexList($table)
|
||||||
|
{
|
||||||
|
$indexes = $this->query("EXEC sp_helpindex '$table';");
|
||||||
|
$indexList = array();
|
||||||
|
|
||||||
|
// Enumerate all basic indexes
|
||||||
|
foreach ($indexes as $index) {
|
||||||
|
if (strpos($index['index_description'], 'unique') !== false) {
|
||||||
|
$indexType = 'unique ';
|
||||||
|
} else {
|
||||||
|
$indexType = 'index ';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract name from index
|
||||||
|
$baseIndexName = $this->buildMSSQLIndexName($table, '');
|
||||||
|
$indexName = substr($index['index_name'], strlen($baseIndexName));
|
||||||
|
|
||||||
|
// Extract columns
|
||||||
|
$columns = $this->quoteColumnSpecString($index['index_keys']);
|
||||||
|
$indexList[$indexName] = $this->parseIndexSpec($indexName, array(
|
||||||
|
'name' => $indexName,
|
||||||
|
'value' => $columns,
|
||||||
|
'type' => $indexType
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we need to check to see if we have any fulltext indexes attached to this table:
|
||||||
|
if ($this->database->fullTextEnabled()) {
|
||||||
|
$result = $this->query('EXEC sp_help_fulltext_columns;');
|
||||||
|
|
||||||
|
// Extract columns from this fulltext definition
|
||||||
|
$columns = array();
|
||||||
|
foreach ($result as $row) {
|
||||||
|
if ($row['TABLE_NAME'] == $table) {
|
||||||
|
$columns[] = $row['FULLTEXT_COLUMN_NAME'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($columns)) {
|
||||||
|
$indexList['SearchFields'] = $this->parseIndexSpec('SearchFields', array(
|
||||||
|
'name' => 'SearchFields',
|
||||||
|
'value' => $this->implodeColumnList($columns),
|
||||||
|
'type' => 'fulltext'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $indexList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For a given table name, get all the internal index names,
|
||||||
|
* except for those that are primary keys and fulltext indexes.
|
||||||
|
*
|
||||||
|
* @param string $tableName
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function indexNames($tableName)
|
||||||
|
{
|
||||||
|
return $this->preparedQuery('
|
||||||
|
SELECT ind.name FROM sys.indexes ind
|
||||||
|
INNER JOIN sys.tables t ON ind.object_id = t.object_id
|
||||||
|
WHERE is_primary_key = 0 AND t.name = ? AND ind.name IS NOT NULL',
|
||||||
|
array($tableName)
|
||||||
|
)->column();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tableList()
|
||||||
|
{
|
||||||
|
$tables = array();
|
||||||
|
foreach ($this->query("EXEC sp_tables @table_owner = 'dbo';") as $record) {
|
||||||
|
$tables[strtolower($record['TABLE_NAME'])] = $record['TABLE_NAME'];
|
||||||
|
}
|
||||||
|
return $tables;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a boolean type-formatted string
|
||||||
|
* We use 'bit' so that we can do numeric-based comparisons
|
||||||
|
*
|
||||||
|
* @param array $values Contains a tokenised list of info about this data type
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function boolean($values)
|
||||||
|
{
|
||||||
|
$default = ($values['default']) ? '1' : '0';
|
||||||
|
return 'bit not null default ' . $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a date type-formatted string.
|
||||||
|
*
|
||||||
|
* @param array $values Contains a tokenised list of info about this data type
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function date($values)
|
||||||
|
{
|
||||||
|
return 'date null';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a decimal type-formatted string
|
||||||
|
*
|
||||||
|
* @param array $values Contains a tokenised list of info about this data type
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function decimal($values)
|
||||||
|
{
|
||||||
|
// Avoid empty strings being put in the db
|
||||||
|
if ($values['precision'] == '') {
|
||||||
|
$precision = 1;
|
||||||
|
} else {
|
||||||
|
$precision = $values['precision'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$defaultValue = '0';
|
||||||
|
if (isset($values['default']) && is_numeric($values['default'])) {
|
||||||
|
$defaultValue = $values['default'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return "decimal($precision) not null default $defaultValue";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a enum type-formatted string
|
||||||
|
*
|
||||||
|
* @param array $values Contains a tokenised list of info about this data type
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function enum($values)
|
||||||
|
{
|
||||||
|
// Enums are a bit different. We'll be creating a varchar(255) with a constraint of all the
|
||||||
|
// usual enum options.
|
||||||
|
// NOTE: In this one instance, we are including the table name in the values array
|
||||||
|
|
||||||
|
$maxLength = max(array_map('strlen', $values['enums']));
|
||||||
|
|
||||||
|
return "varchar($maxLength) not null default '" . $values['default']
|
||||||
|
. "' check(\"" . $values['name'] . "\" in ('" . implode("','", $values['enums'])
|
||||||
|
. "'))";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @todo Make this work like {@link MySQLDatabase::set()}
|
||||||
|
*
|
||||||
|
* @param array $values
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function set($values)
|
||||||
|
{
|
||||||
|
return $this->enum($values);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a float type-formatted string.
|
||||||
|
*
|
||||||
|
* @param array $values Contains a tokenised list of info about this data type
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function float($values)
|
||||||
|
{
|
||||||
|
return 'float(53) not null default ' . $values['default'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a int type-formatted string
|
||||||
|
*
|
||||||
|
* @param array $values Contains a tokenised list of info about this data type
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function int($values)
|
||||||
|
{
|
||||||
|
return 'int not null default ' . (int) $values['default'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a bigint type-formatted string
|
||||||
|
*
|
||||||
|
* @param array $values Contains a tokenised list of info about this data type
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function bigint($values)
|
||||||
|
{
|
||||||
|
return 'bigint not null default ' . (int) $values['default'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a datetime type-formatted string
|
||||||
|
* For MS SQL, we simply return the word 'timestamp', no other parameters are necessary
|
||||||
|
*
|
||||||
|
* @param array $values Contains a tokenised list of info about this data type
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function datetime($values)
|
||||||
|
{
|
||||||
|
return 'datetime null';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a text type-formatted string
|
||||||
|
*
|
||||||
|
* @param array $values Contains a tokenised list of info about this data type
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function text($values)
|
||||||
|
{
|
||||||
|
$collation = MSSQLDatabase::get_collation();
|
||||||
|
$collationSQL = $collation ? " COLLATE $collation" : "";
|
||||||
|
return "nvarchar(max)$collationSQL null";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a time type-formatted string.
|
||||||
|
*
|
||||||
|
* @param array $values Contains a tokenised list of info about this data type
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function time($values)
|
||||||
|
{
|
||||||
|
return 'time null';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a varchar type-formatted string
|
||||||
|
*
|
||||||
|
* @param array $values Contains a tokenised list of info about this data type
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function varchar($values)
|
||||||
|
{
|
||||||
|
$collation = MSSQLDatabase::get_collation();
|
||||||
|
$collationSQL = $collation ? " COLLATE $collation" : "";
|
||||||
|
return "nvarchar(" . $values['precision'] . ")$collationSQL null";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a 4 digit numeric type.
|
||||||
|
*
|
||||||
|
* @param array $values
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function year($values)
|
||||||
|
{
|
||||||
|
return 'numeric(4)';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This returns the column which is the primary key for each table
|
||||||
|
*
|
||||||
|
* @param bool $asDbValue
|
||||||
|
* @param bool $hasAutoIncPK
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function IdColumn($asDbValue = false, $hasAutoIncPK = true)
|
||||||
|
{
|
||||||
|
if ($asDbValue) {
|
||||||
|
return 'int not null';
|
||||||
|
} elseif ($hasAutoIncPK) {
|
||||||
|
return 'int identity(1,1)';
|
||||||
|
} else {
|
||||||
|
return 'int not null';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasTable($tableName)
|
||||||
|
{
|
||||||
|
return (bool)$this->preparedQuery(
|
||||||
|
"SELECT table_name FROM information_schema.tables WHERE table_name = ?",
|
||||||
|
array($tableName)
|
||||||
|
)->value();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the values of the given enum field
|
||||||
|
* NOTE: Experimental; introduced for db-abstraction and may changed before 2.4 is released.
|
||||||
|
*
|
||||||
|
* @param string $tableName
|
||||||
|
* @param string $fieldName
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function enumValuesForField($tableName, $fieldName)
|
||||||
|
{
|
||||||
|
$classes = array();
|
||||||
|
|
||||||
|
// Get the enum of all page types from the SiteTree table
|
||||||
|
$clause = $this->getConstraintCheckClause($tableName, $fieldName);
|
||||||
|
if ($clause) {
|
||||||
|
$classes = $this->enumValuesFromCheckClause($clause);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a lookup table for data types.
|
||||||
|
*
|
||||||
|
* For instance, MSSQL uses 'BIGINT', while MySQL uses 'UNSIGNED'
|
||||||
|
* and PostgreSQL uses 'INT'.
|
||||||
|
*
|
||||||
|
* @param string $type
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function dbDataType($type)
|
||||||
|
{
|
||||||
|
$values = array(
|
||||||
|
'unsigned integer'=>'BIGINT'
|
||||||
|
);
|
||||||
|
if (isset($values[$type])) {
|
||||||
|
return $values[$type];
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function indexKey($table, $index, $spec)
|
||||||
|
{
|
||||||
|
return $index;
|
||||||
|
}
|
||||||
|
}
|
214
code/SQLServerConnector.php
Normal file
214
code/SQLServerConnector.php
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\MSSQL;
|
||||||
|
|
||||||
|
use SilverStripe\ORM\Connect\DBConnector;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database connector driver for sqlsrv_ library
|
||||||
|
*/
|
||||||
|
class SQLServerConnector extends DBConnector
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connection to the DBMS.
|
||||||
|
*
|
||||||
|
* @var resource
|
||||||
|
*/
|
||||||
|
protected $dbConn = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the affected rows of the last query.
|
||||||
|
* Used by sqlsrv functions only, as sqlsrv_rows_affected
|
||||||
|
* accepts a result instead of a database handle.
|
||||||
|
*
|
||||||
|
* @var integer
|
||||||
|
*/
|
||||||
|
protected $lastAffectedRows;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of the currently selected database
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $selectedDatabase = null;
|
||||||
|
|
||||||
|
public function connect($parameters, $selectDB = false)
|
||||||
|
{
|
||||||
|
// Disable default warnings as errors behaviour for sqlsrv to keep it in line with mssql functions
|
||||||
|
if (ini_get('sqlsrv.WarningsReturnAsErrors')) {
|
||||||
|
ini_set('sqlsrv.WarningsReturnAsErrors', 'Off');
|
||||||
|
}
|
||||||
|
|
||||||
|
$charset = isset($parameters['charset']) ? $parameters : 'UTF-8';
|
||||||
|
$multiResultSets = isset($parameters['multipleactiveresultsets'])
|
||||||
|
? $parameters['multipleactiveresultsets']
|
||||||
|
: true;
|
||||||
|
$options = array(
|
||||||
|
'CharacterSet' => $charset,
|
||||||
|
'MultipleActiveResultSets' => $multiResultSets
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!(defined('MSSQL_USE_WINDOWS_AUTHENTICATION') && MSSQL_USE_WINDOWS_AUTHENTICATION == true)
|
||||||
|
&& empty($parameters['windowsauthentication'])
|
||||||
|
) {
|
||||||
|
$options['UID'] = $parameters['username'];
|
||||||
|
$options['PWD'] = $parameters['password'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Required by MS Azure database
|
||||||
|
if ($selectDB && !empty($parameters['database'])) {
|
||||||
|
$options['Database'] = $parameters['database'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->dbConn = sqlsrv_connect($parameters['server'], $options);
|
||||||
|
|
||||||
|
if (empty($this->dbConn)) {
|
||||||
|
$this->databaseError("Couldn't connect to SQL Server database");
|
||||||
|
} elseif ($selectDB && !empty($parameters['database'])) {
|
||||||
|
// Check selected database (Azure)
|
||||||
|
$this->selectedDatabase = $parameters['database'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start transaction. READ ONLY not supported.
|
||||||
|
*/
|
||||||
|
public function transactionStart()
|
||||||
|
{
|
||||||
|
$result = sqlsrv_begin_transaction($this->dbConn);
|
||||||
|
if (!$result) {
|
||||||
|
$this->databaseError("Couldn't start the transaction.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commit everything inside this transaction so far
|
||||||
|
*/
|
||||||
|
public function transactionEnd()
|
||||||
|
{
|
||||||
|
$result = sqlsrv_commit($this->dbConn);
|
||||||
|
if (!$result) {
|
||||||
|
$this->databaseError("Couldn't commit the transaction.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rollback or revert to a savepoint if your queries encounter problems
|
||||||
|
* If you encounter a problem at any point during a transaction, you may
|
||||||
|
* need to rollback that particular query, or return to a savepoint
|
||||||
|
*/
|
||||||
|
public function transactionRollback()
|
||||||
|
{
|
||||||
|
$result = sqlsrv_rollback($this->dbConn);
|
||||||
|
if (!$result) {
|
||||||
|
$this->databaseError("Couldn't rollback the transaction.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function affectedRows()
|
||||||
|
{
|
||||||
|
return $this->lastAffectedRows;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLastError()
|
||||||
|
{
|
||||||
|
$errorMessages = array();
|
||||||
|
$errors = sqlsrv_errors();
|
||||||
|
if ($errors) {
|
||||||
|
foreach ($errors as $info) {
|
||||||
|
$errorMessages[] = implode(', ', array($info['SQLSTATE'], $info['code'], $info['message']));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return implode('; ', $errorMessages);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isActive()
|
||||||
|
{
|
||||||
|
return $this->dbConn && $this->selectedDatabase;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR)
|
||||||
|
{
|
||||||
|
// Reset state
|
||||||
|
$this->lastAffectedRows = 0;
|
||||||
|
|
||||||
|
// Run query
|
||||||
|
$parsedParameters = $this->parameterValues($parameters);
|
||||||
|
if (empty($parsedParameters)) {
|
||||||
|
$handle = sqlsrv_query($this->dbConn, $sql);
|
||||||
|
} else {
|
||||||
|
$handle = sqlsrv_query($this->dbConn, $sql, $parsedParameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for error
|
||||||
|
if (!$handle) {
|
||||||
|
$this->databaseError($this->getLastError(), $errorLevel, $sql, $parsedParameters);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report result
|
||||||
|
$this->lastAffectedRows = sqlsrv_rows_affected($handle);
|
||||||
|
return new SQLServerQuery($this, $handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function query($sql, $errorLevel = E_USER_ERROR)
|
||||||
|
{
|
||||||
|
return $this->preparedQuery($sql, array(), $errorLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function selectDatabase($name)
|
||||||
|
{
|
||||||
|
$this->query("USE \"$name\"");
|
||||||
|
$this->selectedDatabase = $name;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __destruct()
|
||||||
|
{
|
||||||
|
if (is_resource($this->dbConn)) {
|
||||||
|
sqlsrv_close($this->dbConn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getVersion()
|
||||||
|
{
|
||||||
|
// @todo - use sqlsrv_server_info?
|
||||||
|
return trim($this->query("SELECT CONVERT(char(15), SERVERPROPERTY('ProductVersion'))")->value());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getGeneratedID($table)
|
||||||
|
{
|
||||||
|
return $this->query("SELECT IDENT_CURRENT('$table')")->value();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSelectedDatabase()
|
||||||
|
{
|
||||||
|
return $this->selectedDatabase;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function unloadDatabase()
|
||||||
|
{
|
||||||
|
$this->selectDatabase('Master');
|
||||||
|
$this->selectedDatabase = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quotes a string, including the "N" prefix so unicode
|
||||||
|
* strings are saved to the database correctly.
|
||||||
|
*
|
||||||
|
* @param string $value String to be encoded
|
||||||
|
* @return string Processed string ready for DB
|
||||||
|
*/
|
||||||
|
public function quoteString($value)
|
||||||
|
{
|
||||||
|
return "N'" . $this->escapeString($value) . "'";
|
||||||
|
}
|
||||||
|
|
||||||
|
public function escapeString($value)
|
||||||
|
{
|
||||||
|
$value = str_replace("'", "''", $value);
|
||||||
|
$value = str_replace("\0", "[NULL]", $value);
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
}
|
76
code/SQLServerQuery.php
Normal file
76
code/SQLServerQuery.php
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace SilverStripe\MSSQL;
|
||||||
|
|
||||||
|
use DateTime;
|
||||||
|
use SilverStripe\ORM\Connect\Query;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A result-set from a MSSQL database.
|
||||||
|
*/
|
||||||
|
class SQLServerQuery extends Query
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The SQLServerConnector object that created this result set.
|
||||||
|
*
|
||||||
|
* @var SQLServerConnector
|
||||||
|
*/
|
||||||
|
private $connector;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The internal MSSQL handle that points to the result set.
|
||||||
|
*
|
||||||
|
* @var resource
|
||||||
|
*/
|
||||||
|
private $handle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook the result-set given into a Query class, suitable for use by sapphire.
|
||||||
|
* @param SQLServerConnector $connector The database object that created this query.
|
||||||
|
* @param resource $handle the internal mssql handle that is points to the resultset.
|
||||||
|
*/
|
||||||
|
public function __construct(SQLServerConnector $connector, $handle)
|
||||||
|
{
|
||||||
|
$this->connector = $connector;
|
||||||
|
$this->handle = $handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __destruct()
|
||||||
|
{
|
||||||
|
if (is_resource($this->handle)) {
|
||||||
|
sqlsrv_free_stmt($this->handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getIterator()
|
||||||
|
{
|
||||||
|
if (is_resource($this->handle)) {
|
||||||
|
while ($data = sqlsrv_fetch_array($this->handle, SQLSRV_FETCH_ASSOC)) {
|
||||||
|
// special case for sqlsrv - date values are DateTime coming out of the sqlsrv drivers,
|
||||||
|
// so we convert to the usual Y-m-d H:i:s value!
|
||||||
|
foreach ($data as $name => $value) {
|
||||||
|
if ($value instanceof DateTime) {
|
||||||
|
$data[$name] = $value->format('Y-m-d H:i:s');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
yield $data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function numRecords()
|
||||||
|
{
|
||||||
|
if (!is_resource($this->handle)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// WARNING: This will only work if the cursor type is scrollable!
|
||||||
|
if (function_exists('sqlsrv_num_rows')) {
|
||||||
|
return sqlsrv_num_rows($this->handle);
|
||||||
|
} else {
|
||||||
|
user_error('MSSQLQuery::numRecords() not supported in this version of sqlsrv', E_USER_WARNING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
35
composer.json
Normal file
35
composer.json
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "silverstripe/mssql",
|
||||||
|
"description": "Adds MSSQL support to SilverStripe",
|
||||||
|
"type": "silverstripe-vendormodule",
|
||||||
|
"keywords": ["silverstripe", "mssql", "database"],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Sam Minnee",
|
||||||
|
"email": "sam@silverstripe.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Sean Harvey",
|
||||||
|
"email": "sean@silverstripe.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"silverstripe/framework": "^4"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-sqlsrv": "Required to support MSSQLDatabase as the server type",
|
||||||
|
"ext-pdo_sqlsrv": "Required to support MSSQLPDODatabase as the server type"
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "3.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"SilverStripe\\MSSQL\\": "code/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"prefer-stable": true,
|
||||||
|
"minimum-stability": "dev"
|
||||||
|
}
|
35
tests/MSSQLDatabaseQueryTest.php
Normal file
35
tests/MSSQLDatabaseQueryTest.php
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use SilverStripe\ORM\DataObject;
|
||||||
|
use SilverStripe\Dev\SapphireTest;
|
||||||
|
use SilverStripe\Dev\TestOnly;
|
||||||
|
|
||||||
|
class MSSQLDatabaseQueryTest extends SapphireTest
|
||||||
|
{
|
||||||
|
|
||||||
|
public static $fixture_file = 'MSSQLDatabaseQueryTest.yml';
|
||||||
|
|
||||||
|
protected $extraDataObjects = array(
|
||||||
|
'MSSQLDatabaseQueryTestDataObject'
|
||||||
|
);
|
||||||
|
|
||||||
|
public function testDateValueFormatting()
|
||||||
|
{
|
||||||
|
$obj = $this->objFromFixture('MSSQLDatabaseQueryTestDataObject', 'test-data-1');
|
||||||
|
$this->assertEquals('2012-01-01', $obj->obj('TestDate')->Format('Y-m-d'), 'Date field value is formatted correctly (Y-m-d)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDatetimeValueFormatting()
|
||||||
|
{
|
||||||
|
$obj = $this->objFromFixture('MSSQLDatabaseQueryTestDataObject', 'test-data-1');
|
||||||
|
$this->assertEquals('2012-01-01 10:30:00', $obj->obj('TestDatetime')->Format('Y-m-d H:i:s'), 'Datetime field value is formatted correctly (Y-m-d H:i:s)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class MSSQLDatabaseQueryTestDataObject extends DataObject implements TestOnly
|
||||||
|
{
|
||||||
|
|
||||||
|
private static $db = array(
|
||||||
|
'TestDate' => 'Date',
|
||||||
|
'TestDatetime' => 'Datetime'
|
||||||
|
);
|
||||||
|
}
|
5
tests/MSSQLDatabaseQueryTest.yml
Normal file
5
tests/MSSQLDatabaseQueryTest.yml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
MSSQLDatabaseQueryTestDataObject:
|
||||||
|
test-data-1:
|
||||||
|
TestDate: 2012-01-01
|
||||||
|
TestDatetime: 2012-01-01 10:30:00
|
||||||
|
|
Loading…
Reference in New Issue
Block a user