2009-02-25 06:44:52 +01:00
< ? php
2009-04-22 00:19:45 +02:00
/**
2010-05-28 00:25:45 +02:00
* Microsoft SQL Server 2008 + connector class .
2011-01-12 03:05:32 +01:00
*
2010-05-26 12:05:21 +02:00
* < h2 > Connecting using Windows </ h2 >
2011-01-12 03:05:32 +01:00
*
2009-10-22 06:02:40 +02:00
* If you 've got your website running on Windows, it' s highly recommended you
2010-05-26 12:05:21 +02:00
* use Microsoft SQL Server Driver for PHP " sqlsrv " .
2011-01-12 03:05:32 +01:00
*
2010-08-30 07:47:57 +02:00
* A complete guide to installing a Windows IIS + PHP + SQL Server web stack can be
* found here : http :// doc . silverstripe . org / installation - on - windows - server - manual - iis
2011-01-12 03:05:32 +01:00
*
2010-05-26 12:05:21 +02:00
* @ see http :// sqlsrvphp . codeplex . com /
2011-01-12 03:05:32 +01:00
*
2010-05-26 12:05:21 +02:00
* < h2 > Connecting using Linux or Mac OS X </ h2 >
2011-01-12 03:05:32 +01:00
*
2010-05-28 00:25:45 +02:00
* The following commands assume you used the default package manager
* to install PHP with the operating system .
2011-01-12 03:05:32 +01:00
*
2010-05-28 00:25:45 +02:00
* Debian , and Ubuntu :
* < code > apt - get install php5 - sybase </ code >
*
* Fedora , CentOS and RedHat :
* < code > yum install php - mssql </ code >
2011-01-12 03:05:32 +01:00
*
2010-05-28 00:25:45 +02:00
* Mac OS X ( MacPorts ) :
* < code > port install php5 - mssql </ code >
2011-01-12 03:05:32 +01:00
*
2010-05-28 00:25:45 +02:00
* These packages will install the mssql extension for PHP , as well
2010-08-30 07:47:57 +02:00
* as FreeTDS , which will let you connect to SQL Server .
2011-01-12 03:05:32 +01:00
*
2010-05-26 12:05:21 +02:00
* More information available in the SilverStripe developer wiki :
* @ see http :// doc . silverstripe . org / modules : mssql
2010-08-30 07:47:57 +02:00
* @ see http :// doc . silverstripe . org / installation - on - windows - server - manual - iis
2011-01-12 03:05:32 +01:00
*
2010-05-28 00:25:45 +02:00
* References :
* @ see http :// freetds . org
2011-01-12 03:05:32 +01:00
*
2009-10-22 06:01:17 +02:00
* @ package mssql
2009-04-22 00:19:45 +02:00
*/
2009-10-26 21:59:29 +01:00
class MSSQLDatabase extends SS_Database {
2011-01-12 03:05:32 +01:00
2009-02-25 06:44:52 +01:00
/**
* Connection to the DBMS .
* @ var resource
*/
2010-02-01 22:45:01 +01:00
protected $dbConn ;
2011-01-12 03:05:32 +01:00
2009-02-25 06:44:52 +01:00
/**
* True if we are connected to a database .
* @ var boolean
*/
2010-02-01 22:45:01 +01:00
protected $active ;
2011-01-12 03:05:32 +01:00
2009-02-25 06:44:52 +01:00
/**
* The name of the database .
* @ var string
*/
2010-02-01 22:45:01 +01:00
protected $database ;
2011-01-12 03:05:32 +01:00
2009-05-28 04:07:11 +02:00
/**
* If true , use the mssql_ ... functions .
* If false use the sqlsrv_ ... functions
*/
2010-02-01 22:45:01 +01:00
protected $mssql = null ;
2011-01-12 03:05:32 +01:00
2010-03-11 09:47:04 +01:00
/**
* 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 ;
2009-05-28 04:07:11 +02:00
2009-07-09 03:11:02 +02:00
/**
* Words that will trigger an error if passed to a SQL Server fulltext search
*/
2010-09-28 21:44:08 +02:00
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' );
2010-08-31 05:18:00 +02:00
/**
* Transactions will work with FreeTDS , but not entirely with sqlsrv driver on Windows with MARS enabled .
* TODO :
2011-01-12 03:05:32 +01:00
* - after the test fails with open transaction , the transaction should be rolled back ,
2010-08-31 05:18:00 +02:00
* otherwise other tests will break claiming that transaction is still open .
* - figure out SAVEPOINTS
* - READ ONLY transactions
*/
2010-10-02 05:36:04 +02:00
protected $supportsTransactions = true ;
2011-01-12 03:05:32 +01:00
2010-02-23 02:23:42 +01:00
/**
* Cached flag to determine if full - text is enabled . This is set by
* { @ link MSSQLDatabase :: fullTextEnabled ()}
2011-01-12 03:05:32 +01:00
*
2010-02-23 02:23:42 +01:00
* @ var boolean
*/
protected $fullTextEnabled = null ;
2010-10-14 23:11:20 +02:00
/**
* Stores per - request cached constraint checks that come from the database .
* @ var array
*/
protected static $cached_checks = array ();
2010-06-03 02:58:31 +02:00
/**
* @ ignore
*/
protected static $collation = null ;
2010-10-14 23:11:20 +02:00
2010-06-03 02:58:31 +02:00
/**
* 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 .
*/
public static function set_collation ( $collation ) {
self :: $collation = $collation ;
}
2011-01-12 03:05:32 +01:00
2009-02-25 06:44:52 +01:00
/**
* Connect to a MS SQL database .
* @ param array $parameters An map of parameters , which should include :
* - 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
*/
public function __construct ( $parameters ) {
2009-05-28 04:07:11 +02:00
if ( function_exists ( 'mssql_connect' )) {
$this -> mssql = true ;
} 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 );
}
2010-09-29 06:30:05 +02:00
2009-05-28 04:07:11 +02:00
if ( $this -> mssql ) {
2010-06-03 02:58:31 +02:00
// Switch to utf8 connection charset
ini_set ( 'mssql.charset' , 'utf8' );
2009-10-16 00:04:55 +02:00
$this -> dbConn = mssql_connect ( $parameters [ 'server' ], $parameters [ 'username' ], $parameters [ 'password' ], true );
2009-05-28 04:07:11 +02:00
} else {
2010-03-05 03:40:20 +01:00
// Disable default warnings as errors behaviour for sqlsrv to keep it in line with mssql functions
2010-03-11 10:44:51 +01:00
if ( ini_get ( 'sqlsrv.WarningsReturnAsErrors' )) {
2010-03-05 03:40:20 +01:00
ini_set ( 'sqlsrv.WarningsReturnAsErrors' , 'Off' );
}
2010-09-29 06:30:05 +02:00
$options = array (
'CharacterSet' => 'UTF-8' ,
'MultipleActiveResultSets' => true
);
if ( ! ( defined ( 'MSSQL_USE_WINDOWS_AUTHENTICATION' ) && MSSQL_USE_WINDOWS_AUTHENTICATION == true )) {
$options [ 'UID' ] = $parameters [ 'username' ];
$options [ 'PWD' ] = $parameters [ 'password' ];
}
$this -> dbConn = sqlsrv_connect ( $parameters [ 'server' ], $options );
2009-05-28 04:07:11 +02:00
}
2009-05-27 02:36:53 +02:00
2009-02-25 06:44:52 +01:00
if ( ! $this -> dbConn ) {
2010-09-29 06:30:05 +02:00
$this -> databaseError ( 'Couldn\'t connect to SQL Server database' );
2009-02-25 06:44:52 +01:00
} else {
$this -> database = $parameters [ 'database' ];
2009-05-27 02:36:53 +02:00
$this -> selectDatabase ( $this -> database );
2009-02-25 06:44:52 +01:00
2009-05-27 02:36:53 +02:00
// Configure the connection
$this -> query ( 'SET QUOTED_IDENTIFIER ON' );
2009-05-27 02:51:47 +02:00
$this -> query ( 'SET TEXTSIZE 2147483647' );
2009-05-27 02:36:53 +02:00
}
2009-02-25 06:44:52 +01:00
}
2011-01-12 03:05:32 +01:00
2009-07-20 09:23:38 +02:00
public function __destruct () {
2009-07-28 06:07:42 +02:00
if ( is_resource ( $this -> dbConn )) {
2009-10-22 06:01:17 +02:00
if ( $this -> mssql ) {
mssql_close ( $this -> dbConn );
} else {
sqlsrv_close ( $this -> dbConn );
}
2009-07-20 09:23:38 +02:00
}
}
2011-01-12 03:05:32 +01:00
2012-12-18 23:27:49 +01:00
/**
* Return the last database error .
* @ return string
*/
public function getLastError () {
$error = '' ;
if ( $this -> mssql ) {
$error = mssql_get_last_message ();
} else {
$errors = sqlsrv_errors ();
if ( $errors ) foreach ( $errors as $info ) {
$error .= implode ( ', ' , array ( $info [ 'SQLSTATE' ], $info [ 'code' ], $info [ 'message' ]));
}
}
return $error ;
}
2010-02-03 09:11:31 +01:00
/**
2010-02-12 00:09:37 +01:00
* Checks whether the current SQL Server version has full - text
* support installed and full - text is enabled for this database .
2011-01-12 03:05:32 +01:00
*
2010-02-03 09:11:31 +01:00
* @ return boolean
*/
public function fullTextEnabled () {
2010-02-23 02:23:42 +01:00
if ( $this -> fullTextEnabled === null ) {
$isInstalled = ( boolean ) DB :: query ( " SELECT fulltextserviceproperty('isfulltextinstalled') " ) -> value ();
$enabledForDb = ( boolean ) DB :: query ( "
SELECT is_fulltext_enabled
FROM sys . databases
WHERE name = '$this->database'
" )->value();
2010-03-11 10:46:16 +01:00
$this -> fullTextEnabled = ( boolean ) ( $isInstalled && $enabledForDb );
2010-02-23 02:23:42 +01:00
}
return $this -> fullTextEnabled ;
2010-02-03 09:11:31 +01:00
}
2011-01-12 03:05:32 +01:00
2009-05-28 04:07:11 +02:00
/**
* Throw a database error
*/
function databaseError ( $message , $errorLevel = E_USER_ERROR ) {
2012-12-18 23:39:51 +01:00
$lastError = $this -> getLastError ();
if ( $lastError ) $message .= " \n Last error: " . $lastError ;
2009-05-28 04:07:11 +02:00
return parent :: databaseError ( $message , $errorLevel );
}
2011-01-12 03:05:32 +01:00
2009-03-17 04:58:58 +01:00
/**
* This will set up the full text search capabilities .
*/
2010-02-03 09:24:33 +01:00
function createFullTextCatalog () {
2011-10-13 03:12:58 +02:00
$result = $this -> fullTextCatalogExists ();
if ( ! $result ) $this -> query ( 'CREATE FULLTEXT CATALOG "ftCatalog" AS DEFAULT;' );
}
/**
* Check that a fulltext catalog has been created yet .
* @ return boolean
*/
public function fullTextCatalogExists () {
return ( bool ) $this -> query ( " SELECT name FROM sys.fulltext_catalogs WHERE name = 'ftCatalog'; " ) -> value ();
}
2011-01-12 03:05:32 +01:00
2010-08-26 22:54:40 +02:00
/**
* 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 .
2010-08-31 07:52:48 +02:00
*
* 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
2010-08-26 22:54:40 +02:00
*
* @ 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'; " );
2011-01-12 03:05:32 +01:00
2010-08-26 22:54:40 +02:00
// 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 ();
2011-01-12 03:05:32 +01:00
2010-08-26 22:54:40 +02:00
if ( isset ( $status [ 'STATUS' ]) && $status [ 'STATUS' ] == 0 ) {
// Idle!
break ;
}
sleep ( 1 );
}
}
}
2011-01-12 03:05:32 +01:00
2009-02-25 06:44:52 +01:00
/**
* Not implemented , needed for PDO
*/
public function getConnect ( $parameters ) {
return null ;
}
2011-01-12 03:05:32 +01:00
2009-02-25 06:44:52 +01:00
/**
* Returns true if this database supports collations
* @ return boolean
*/
public function supportsCollations () {
return true ;
}
2011-01-12 03:05:32 +01:00
2012-07-06 11:35:58 +02:00
public function supportsTimezoneOverride () {
return true ;
}
2009-02-25 06:44:52 +01:00
/**
2009-04-14 00:02:55 +02:00
* Get the version of MSSQL .
2010-05-26 07:24:15 +02:00
* @ return string
2009-02-25 06:44:52 +01:00
*/
public function getVersion () {
2010-05-26 07:24:15 +02:00
return trim ( $this -> query ( " SELECT CONVERT(char(15), SERVERPROPERTY('ProductVersion')) " ) -> value ());
2009-02-25 06:44:52 +01:00
}
2011-01-12 03:05:32 +01:00
2009-02-25 06:44:52 +01:00
/**
2009-04-14 00:02:55 +02:00
* Get the database server , namely mssql .
2009-02-25 06:44:52 +01:00
* @ return string
*/
public function getDatabaseServer () {
return " mssql " ;
}
2011-01-12 03:05:32 +01:00
2009-02-25 06:44:52 +01:00
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 ;
}
2011-01-12 03:05:32 +01:00
if ( isset ( $_REQUEST [ 'showqueries' ])) {
2009-02-25 06:44:52 +01:00
$starttime = microtime ( true );
}
2010-05-11 00:56:43 +02:00
2010-05-11 02:12:13 +02:00
$error = '' ;
2009-05-28 04:07:11 +02:00
if ( $this -> mssql ) {
$handle = mssql_query ( $sql , $this -> dbConn );
} else {
$handle = sqlsrv_query ( $this -> dbConn , $sql );
2010-03-11 09:47:04 +01:00
if ( $handle ) $this -> lastAffectedRows = sqlsrv_rows_affected ( $handle );
2009-05-28 04:07:11 +02:00
}
2010-05-11 00:56:43 +02:00
2009-02-25 06:44:52 +01:00
if ( isset ( $_REQUEST [ 'showqueries' ])) {
$endtime = round ( microtime ( true ) - $starttime , 4 );
Debug :: message ( " \n $sql\n { $endtime } ms \n " , false );
}
2010-05-11 00:56:43 +02:00
2012-12-18 23:27:49 +01:00
if ( ! $handle && $errorLevel ) $this -> databaseError ( " Couldn't run query: $sql " , $errorLevel );
2009-05-28 04:07:11 +02:00
return new MSSQLQuery ( $this , $handle , $this -> mssql );
2009-02-25 06:44:52 +01:00
}
2011-01-12 03:05:32 +01:00
2009-02-25 06:44:52 +01:00
public function getGeneratedID ( $table ) {
2009-03-30 05:06:25 +02:00
return $this -> query ( " SELECT IDENT_CURRENT(' $table ') " ) -> value ();
2009-02-25 06:44:52 +01:00
}
2010-10-20 05:21:54 +02:00
/**
* 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
2009-04-22 00:19:45 +02:00
*/
2011-01-12 03:05:32 +01:00
function getPrimaryKey ( $tableName ){
$indexes = DB :: query ( " EXEC sp_helpindex ' $tableName '; " );
2010-10-20 05:21:54 +02:00
$indexName = '' ;
foreach ( $indexes as $index ) {
if ( $index [ 'index_keys' ] == 'ID' ) {
$indexName = $index [ 'index_name' ];
2009-03-23 01:51:28 +01:00
break ;
}
}
2010-10-20 05:21:54 +02:00
return $indexName ;
2009-03-23 01:51:28 +01:00
}
2010-10-20 05:21:54 +02:00
2010-10-15 02:55:49 +02:00
function getIdentityColumn ( $tableName ) {
return $this -> query ( "
SELECT
TABLE_NAME + '.' + COLUMN_NAME ,
TABLE_NAME
FROM
INFORMATION_SCHEMA . COLUMNS
WHERE
TABLE_SCHEMA = 'dbo' AND
COLUMNPROPERTY ( object_id ( TABLE_NAME ), COLUMN_NAME , 'IsIdentity' ) = 1 AND
TABLE_NAME = '$tableName'
" )->value();
}
2011-01-12 03:05:32 +01:00
2009-02-25 06:44:52 +01:00
public function isActive () {
return $this -> active ? true : false ;
}
2011-01-12 03:05:32 +01:00
2009-07-20 11:09:53 +02:00
/**
* Create the database that is currently selected .
2009-04-22 00:19:45 +02:00
*/
2009-02-25 06:44:52 +01:00
public function createDatabase () {
2009-05-28 04:07:11 +02:00
$this -> query ( " CREATE DATABASE \" $this->database\ " " );
2009-06-04 01:35:24 +02:00
$this -> selectDatabase ( $this -> database );
2009-02-25 06:44:52 +01:00
}
/**
* Drop the database that this object is currently connected to .
* Use with caution .
*/
public function dropDatabase () {
2009-06-16 04:13:00 +02:00
$db = $this -> database ;
2009-05-28 04:07:11 +02:00
$this -> selectDatabase ( 'master' );
2009-06-16 04:13:00 +02:00
$this -> query ( " DROP DATABASE \" $db\ " " );
2009-05-28 04:07:11 +02:00
$this -> active = false ;
2009-02-25 06:44:52 +01:00
}
2010-11-04 23:06:29 +01:00
/**
* Drop the given database name .
* Use with caution .
* @ param string $name Database name to drop
*/
public function dropDatabaseByName ( $name ) {
$this -> query ( " DROP DATABASE \" $name\ " " );
}
2009-02-25 06:44:52 +01:00
/**
* Returns the name of the currently selected database
*/
public function currentDatabase () {
return $this -> database ;
}
2011-01-12 03:05:32 +01:00
2009-02-25 06:44:52 +01:00
/**
* Switches to the given database .
2011-01-12 03:05:32 +01:00
*
2009-07-20 11:09:53 +02:00
* If the database doesn ' t exist , you should call
* createDatabase () after calling selectDatabase ()
2011-01-12 03:05:32 +01:00
*
2009-07-20 11:09:53 +02:00
* @ param string $dbname The database name to switch to
2009-02-25 06:44:52 +01:00
*/
public function selectDatabase ( $dbname ) {
$this -> database = $dbname ;
2011-01-12 03:05:32 +01:00
2009-04-28 07:33:26 +02:00
if ( $this -> databaseExists ( $this -> database )) {
2009-05-28 04:07:11 +02:00
if ( $this -> mssql ) {
if ( mssql_select_db ( $this -> database , $this -> dbConn )) {
$this -> active = true ;
}
} else {
$this -> query ( " USE \" $this->database\ " " );
2009-04-28 07:41:43 +02:00
$this -> active = true ;
}
2009-04-28 07:33:26 +02:00
}
2011-01-12 03:05:32 +01:00
2010-02-23 02:23:42 +01:00
$this -> tableList = $this -> fieldList = $this -> indexList = $this -> fullTextEnabled = null ;
2009-02-25 06:44:52 +01:00
}
/**
2010-11-04 23:06:29 +01:00
* Check if the given database exists from { @ link allDatabaseNames ()} .
2009-07-20 11:09:53 +02:00
* @ param string $name Name of database to check exists
2009-04-28 07:33:26 +02:00
* @ return boolean
2009-02-25 06:44:52 +01:00
*/
public function databaseExists ( $name ) {
2010-11-04 23:06:29 +01:00
$databases = $this -> allDatabaseNames ();
foreach ( $databases as $dbname ) {
if ( $dbname == $name ) return true ;
2011-01-12 03:05:32 +01:00
}
2011-10-13 03:12:58 +02:00
return false ;
}
2010-11-04 23:06:29 +01:00
/**
* Return all databases names from the server .
* @ return array
*/
public function allDatabaseNames () {
return $this -> query ( 'SELECT NAME FROM sys.sysdatabases' ) -> column ();
}
2009-05-21 07:10:46 +02:00
/**
* Create a new table .
* @ param $tableName The name of the table
* @ param $fields A map of field names to field types
* @ param $indexes A map of indexes
* @ param $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
* @ return The table name generated . This may be different from the table name , for example with temporary tables .
*/
2009-10-14 07:51:43 +02:00
public function createTable ( $tableName , $fields = null , $indexes = null , $options = null , $advancedOptions = null ) {
2009-02-25 06:44:52 +01:00
$fieldSchemas = $indexSchemas = " " ;
if ( $fields ) foreach ( $fields as $k => $v ) $fieldSchemas .= " \" $k\ " $v , \n " ;
2011-01-12 03:05:32 +01:00
2009-05-21 07:10:46 +02:00
// Temporary tables start with "#" in MSSQL-land
2009-12-15 00:57:30 +01:00
if ( ! empty ( $options [ 'temporary' ])) {
2009-12-15 01:58:58 +01:00
// Randomize the temp table name to avoid conflicts in the tempdb table which derived databases share
$tableName = " # $tableName " . '-' . rand ( 1000000 , 9999999 );
2009-12-15 00:57:30 +01:00
}
2011-01-12 03:05:32 +01:00
2009-02-25 06:44:52 +01:00
$this -> query ( " CREATE TABLE \" $tableName\ " (
2009-12-15 00:57:30 +01:00
$fieldSchemas
primary key ( \ " ID \" )
); " );
2011-01-12 03:05:32 +01:00
2009-03-17 04:58:58 +01:00
//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
2009-05-13 01:07:21 +02:00
if ( $indexes ) foreach ( $indexes as $k => $v ) {
$indexSchemas .= $this -> getIndexSqlDefinition ( $tableName , $k , $v ) . " \n " ;
}
2011-01-12 03:05:32 +01:00
2009-05-13 01:07:21 +02:00
if ( $indexSchemas ) $this -> query ( $indexSchemas );
2011-01-12 03:05:32 +01:00
2009-05-21 07:10:46 +02:00
return $tableName ;
2009-02-25 06:44:52 +01:00
}
/**
* 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
*/
2009-10-14 07:51:43 +02:00
public function alterTable ( $tableName , $newFields = null , $newIndexes = null , $alteredFields = null , $alteredIndexes = null , $alteredOptions = null , $advancedOptions = null ) {
2009-04-22 00:19:45 +02:00
$alterList = array ();
2009-10-22 08:51:35 +02:00
$indexList = $this -> indexList ( $tableName );
2011-01-12 03:05:32 +01:00
2011-10-13 03:12:58 +02:00
// drop any fulltext indexes that exist on the table before altering the structure
if ( $this -> fullTextIndexExists ( $tableName )) {
$alterList [] = " \n DROP FULLTEXT INDEX ON \" $tableName\ " ; " ;
2009-02-25 06:44:52 +01:00
}
2011-01-12 03:05:32 +01:00
2011-10-13 03:12:58 +02:00
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 , $indexList );
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 );
2009-02-25 06:44:52 +01:00
2009-10-22 08:51:35 +02:00
if ( $alterList ) {
foreach ( $alterList as $alteration ) {
if ( $alteration != '' ) {
$this -> query ( $alteration );
2009-04-22 01:14:43 +02:00
}
2009-04-22 00:19:45 +02:00
}
2009-02-25 06:44:52 +01:00
}
}
2010-10-14 23:11:20 +02:00
2009-07-20 11:09:53 +02:00
/**
2010-10-14 23:11:20 +02:00
* Given the table and column name , retrieve the constraint name for that column
* in the table .
*/
public function getConstraintName ( $tableName , $columnName ) {
return $this -> query ( "
SELECT CONSTRAINT_NAME
FROM INFORMATION_SCHEMA . CONSTRAINT_COLUMN_USAGE
WHERE TABLE_NAME = '$tableName' AND COLUMN_NAME = '$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 ) {
if ( isset ( self :: $cached_checks [ $tableName ])) {
if ( ! isset ( self :: $cached_checks [ $tableName ][ $columnName ])) self :: $cached_checks [ $tableName ][ $columnName ] = null ;
return self :: $cached_checks [ $tableName ][ $columnName ];
}
$checks = array ();
foreach ( $this -> query ( "
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 = '$tableName'
" ) as $record ) {
$checks [ $record [ 'COLUMN_NAME' ]] = $record [ 'CHECK_CLAUSE' ];
}
self :: $cached_checks [ $tableName ] = $checks ;
if ( ! isset ( self :: $cached_checks [ $tableName ][ $columnName ])) self :: $cached_checks [ $tableName ][ $columnName ] = null ;
return self :: $cached_checks [ $tableName ][ $columnName ];
2009-04-06 00:23:58 +02:00
}
2009-07-28 00:39:57 +02:00
/**
* Return the name of the default constraint applied to $tableName . $colName .
* Will return null if no such constraint exists
*/
2010-02-01 22:45:01 +01:00
protected function defaultConstraintName ( $tableName , $colName ) {
2009-07-28 00:39:57 +02:00
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();
}
2011-01-12 03:05:32 +01:00
2009-07-20 11:09:53 +02:00
/**
2010-10-15 00:36:55 +02:00
* 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 ) {
2009-04-06 00:23:58 +02:00
array_unshift ( $constraints , substr ( rtrim ( $bits [ $i ], ')' ), 1 , - 1 ));
2012-09-24 05:36:59 +02:00
}
2009-04-06 00:23:58 +02:00
}
return $constraints ;
}
2011-01-12 03:05:32 +01:00
2009-02-25 06:44:52 +01:00
/*
* Creates an ALTER expression for a column in MS SQL
2011-01-12 03:05:32 +01:00
*
2009-02-25 06:44:52 +01:00
* @ 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
*/
2010-02-01 22:45:01 +01:00
protected function alterTableAlterColumn ( $tableName , $colName , $colSpec , $indexList ){
2009-05-19 04:24:27 +02:00
2009-02-25 06:44:52 +01:00
// 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?
2009-05-07 06:55:09 +02:00
$pattern = '/^([\w()]+)\s?((?:not\s)?null)?\s?(default\s[\w\']+)?\s?(check\s?[\w()\'",\s]+)?$/i' ;
2009-05-19 04:24:27 +02:00
$matches = Array ();
2009-02-25 06:44:52 +01:00
preg_match ( $pattern , $colSpec , $matches );
2011-01-12 03:05:32 +01:00
2010-02-02 00:24:02 +01:00
// drop the index if it exists
2009-04-01 05:37:39 +02:00
$alterCol = '' ;
2013-01-07 04:25:31 +01:00
2013-04-25 01:33:54 +02:00
// drop *ALL* indexes on a table before proceeding
// this won't drop primary keys, though
$indexes = $this -> indexNames ( $tableName );
foreach ( $indexes as $indexName ) {
$alterCol = " \n DROP INDEX \" $indexName\ " ON \ " $tableName\ " ; " ;
}
2009-04-22 00:19:45 +02:00
$prefix = " ALTER TABLE \" " . $tableName . " \" " ;
2009-07-28 00:39:57 +02:00
// Remove the old default prior to adjusting the column.
if ( $defaultConstraintName = $this -> defaultConstraintName ( $tableName , $colName )) {
$alterCol .= " ; \n $prefix DROP CONSTRAINT \" $defaultConstraintName\ " " ;
}
2011-01-12 03:05:32 +01:00
2009-02-25 06:44:52 +01:00
if ( isset ( $matches [ 1 ])) {
2009-07-28 23:15:27 +02:00
//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' ){
2011-01-12 03:05:32 +01:00
2009-07-28 23:15:27 +02:00
// SET null / not null
if ( ! empty ( $matches [ 2 ])) $alterCol .= " ; \n $prefix ALTER COLUMN \" $colName\ " $matches [ 1 ] $matches [ 2 ] " ;
2011-06-09 03:56:55 +02:00
else $alterCol .= " ; \n $prefix ALTER COLUMN \" $colName\ " $matches [ 1 ] " ;
2011-01-12 03:05:32 +01:00
2009-07-28 23:15:27 +02:00
// Add a default back
if ( ! empty ( $matches [ 3 ])) $alterCol .= " ; \n $prefix ADD $matches[3] FOR \" $colName\ " " ;
2011-01-12 03:05:32 +01:00
2009-07-28 23:15:27 +02:00
// SET check constraint (The constraint HAS to be dropped)
if ( ! empty ( $matches [ 4 ])) {
2010-10-14 23:11:20 +02:00
$constraint = $this -> getConstraintName ( $tableName , $colName );
if ( $constraint ) {
$alterCol .= " ; \n $prefix DROP CONSTRAINT { $constraint } " ;
}
2009-07-28 23:15:27 +02:00
//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] " ;
}
2009-02-25 06:44:52 +01:00
}
}
2011-10-13 03:12:58 +02:00
2009-02-25 06:44:52 +01:00
return isset ( $alterCol ) ? $alterCol : '' ;
}
2011-01-12 03:05:32 +01:00
2009-02-25 06:44:52 +01:00
public function renameTable ( $oldTableName , $newTableName ) {
2009-06-15 03:34:22 +02:00
$this -> query ( " EXEC sp_rename \" $oldTableName\ " , \ " $newTableName\ " " );
2009-02-25 06:44:52 +01:00
}
2011-01-12 03:05:32 +01:00
2009-02-25 06:44:52 +01:00
/**
* Checks a table ' s integrity and repairs it if necessary .
2009-04-22 00:19:45 +02:00
* NOTE : MSSQL does not appear to support any vacuum or optimise commands
2011-01-12 03:05:32 +01:00
*
2009-02-25 06:44:52 +01:00
* @ 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 ;
}
2011-01-12 03:05:32 +01:00
2009-02-25 06:44:52 +01:00
public function createField ( $tableName , $fieldName , $fieldSpec ) {
$this -> query ( " ALTER TABLE \" $tableName\ " ADD \ " $fieldName\ " $fieldSpec " );
}
2011-01-12 03:05:32 +01:00
2009-02-25 06:44:52 +01:00
/**
* 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 " );
}
/**
* Change the database column name of the given field .
2011-01-12 03:05:32 +01:00
*
2009-02-25 06:44:52 +01:00
* @ param string $tableName The name of the tbale the field is in .
* @ param string $oldName The name of the field to change .
* @ param string $newName The new name of the field
*/
public function renameField ( $tableName , $oldName , $newName ) {
2012-09-24 05:36:59 +02:00
$this -> query ( " EXEC sp_rename @objname = ' $tableName . $oldName ', @newname = ' $newName ', @objtype = 'COLUMN' " );
}
2010-10-14 23:11:20 +02:00
2009-02-25 06:44:52 +01:00
public function fieldList ( $table ) {
//This gets us more information than we need, but I've included it all for the moment....
2010-10-14 23:11:20 +02:00
$fieldRecords = $this -> query ( " SELECT ordinal_position, column_name, data_type, column_default,
2010-06-03 02:58:31 +02:00
is_nullable , character_maximum_length , numeric_precision , numeric_scale , collation_name
2010-10-14 23:11:20 +02:00
FROM information_schema . columns WHERE table_name = '$table'
2009-07-28 00:39:57 +02:00
ORDER BY ordinal_position ; " );
2010-02-02 00:24:02 +01:00
// 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 ();
2009-02-25 06:44:52 +01:00
$output = array ();
2010-02-02 00:24:02 +01:00
foreach ( $fieldRecords as $record ) {
$fields [] = $record ;
}
2011-01-12 03:05:32 +01:00
2010-02-02 00:24:02 +01:00
foreach ( $fields as $field ) {
2009-07-28 00:39:57 +02:00
// Update the data_type field to be a complete column definition string for use by
2009-10-26 22:16:37 +01:00
// SS_Database::requireField()
2009-03-12 03:43:10 +01:00
switch ( $field [ 'data_type' ]){
2009-07-28 00:39:57 +02:00
case 'bigint' :
case 'numeric' :
case 'float' :
case 'bit' :
2009-07-28 05:48:52 +02:00
if ( $field [ 'data_type' ] != 'bigint' && $sizeSuffix = $field [ 'numeric_precision' ]) {
2009-07-28 23:15:27 +02:00
$field [ 'data_type' ] .= " ( $sizeSuffix ) " ;
2009-07-28 00:39:57 +02:00
}
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 ;
2011-01-12 03:05:32 +01:00
2010-06-03 02:58:31 +02:00
case 'nvarchar' :
2009-03-12 03:43:10 +01:00
case 'varchar' :
//Check to see if there's a constraint attached to this column:
2010-10-14 23:11:20 +02:00
$clause = $this -> getConstraintCheckClause ( $table , $field [ 'column_name' ]);
if ( $clause ) {
2010-10-15 00:36:55 +02:00
$constraints = $this -> enumValuesFromCheckClause ( $clause );
2011-01-12 03:05:32 +01:00
$default = substr ( $field [ 'column_default' ], 2 , - 2 );
2010-10-15 00:36:55 +02:00
$field [ 'data_type' ] = $this -> enum ( array (
'default' => $default ,
'name' => $field [ 'column_name' ],
'enums' => $constraints ,
'table' => $table
));
2009-07-28 00:39:57 +02:00
break ;
2009-03-12 03:43:10 +01:00
}
2009-07-28 00:39:57 +02:00
2009-02-25 06:44:52 +01:00
default :
2009-07-28 00:39:57 +02:00
$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 ' " ;
}
2009-02-25 06:44:52 +01:00
}
2009-07-28 00:39:57 +02:00
$output [ $field [ 'column_name' ]] = $field ;
2011-01-12 03:05:32 +01:00
2009-02-25 06:44:52 +01:00
}
2011-01-12 03:05:32 +01:00
2009-02-25 06:44:52 +01:00
return $output ;
}
2011-01-12 03:05:32 +01:00
/**
*
* This is a stub function . Postgres caches the fieldlist results .
*
* @ param string $tableName
*
* @ return boolean
*/
function clear_cached_fieldlist ( $tableName = false ){
return true ;
}
2009-02-25 06:44:52 +01:00
/**
* Create an index on a table .
* @ param string $tableName The name of the table .
* @ param string $indexName The name of the index .
2009-10-27 00:44:08 +01:00
* @ param string $indexSpec The specification of the index , see SS_Database :: requireIndex () for more details .
2009-02-25 06:44:52 +01:00
*/
public function createIndex ( $tableName , $indexName , $indexSpec ) {
$this -> query ( $this -> getIndexSqlDefinition ( $tableName , $indexName , $indexSpec ));
}
2011-01-12 03:05:32 +01:00
2009-10-22 08:51:35 +02:00
/**
2009-02-25 06:44:52 +01:00
* 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 ){
2012-09-24 05:36:59 +02:00
$indexSpec = $this -> parseIndexSpec ( null , $indexSpec );
return " { $indexSpec [ 'type' ] } ( { $indexSpec [ 'value' ] } ) " ;
}
/**
* Splits a spec string safely , considering quoted columns , whitespace ,
* and cleaning brackets
* @ param string $spec The input index specification
* @ return array List of columns in the spec
*/
function explodeColumnString ( $spec ) {
// Remove any leading/trailing brackets and outlying modifiers
// E.g. 'unique (Title, "QuotedColumn");' => 'Title, "QuotedColumn"'
$containedSpec = preg_replace ( '/(.*\(\s*)|(\s*\).*)/' , '' , $spec );
// Split potentially quoted modifiers
// E.g. 'Title, "QuotedColumn"' => array('Title', 'QuotedColumn')
return preg_split ( '/"?\s*,\s*"?/' , trim ( $containedSpec , '(") ' ));
}
/**
* Builds a properly quoted column list from an array
* @ param array $columns List of columns to implode
* @ return string A properly quoted list of column names
*/
function implodeColumnList ( $columns ) {
if ( empty ( $columns )) return '' ;
return '"' . implode ( '","' , $columns ) . '"' ;
}
/**
* Given an index specification in the form of a string ensure that each
* column name is property quoted , stripping brackets and modifiers .
* This index may also be in the form of a " CREATE INDEX... " sql fragment
* @ param string $spec The input specification or query . E . g . 'unique (Column1, Column2)'
* @ return string The properly quoted column list . E . g . '"Column1", "Column2"'
*/
function quoteColumnSpecString ( $spec ) {
$bits = $this -> explodeColumnString ( $spec );
return $this -> implodeColumnList ( $bits );
}
/**
* Given an index spec determines the index type
* @ param type $spec
* @ return string
*/
function determineIndexType ( $spec ) {
// check array spec
if ( is_array ( $spec ) && isset ( $spec [ 'type' ])) {
return $spec [ 'type' ];
} elseif ( ! is_array ( $spec ) && preg_match ( '/(?<type>\w+)\s*\(/' , $spec , $matchType )) {
return strtolower ( $matchType [ 'type' ]);
} else {
return 'index' ;
}
}
/**
* Converts an array or string index spec into a universally useful array
* @ see convertIndexSpec () for approximate inverse
* @ param string | array $spec An index specification in either one of two formats :
* < ul >
* < li > A string in the format < code > " indextype (Column1, Column2, Column3) " </ code > indextype
* may be omitted , in which case it will be treated as an 'index' type </ li >
* < li > An associative array with the keys 'name' ( which should max the $name
* argument ), 'value' ( in the same format as the above column list ) and
* 'type' .</ li >
* </ ul >
* Index types may be one of unique , index , fulltext , or any adaptor specific
* format . Column names may be double quoted .
* @ return array The resulting spec array with the required fields 'name' ,
* 'type' , and 'value' . The index value will have cleanly quoted column names .
* E . g . < code > array (
* 'type' => 'unique' ,
* 'name' => 'MyIndex' ,
* 'value' => '"Title", "Content"'
* ) </ code >
*/
function parseIndexSpec ( $name , $spec ) {
// Do minimal cleanup on any already parsed spec
if ( is_array ( $spec )) {
$spec [ 'value' ] = $this -> quoteColumnSpecString ( $spec [ 'value' ]);
return $spec ;
2009-02-25 06:44:52 +01:00
}
2012-09-24 05:36:59 +02:00
// Nicely formatted spec!
return array (
'name' => $name ,
'value' => $this -> quoteColumnSpecString ( $spec ),
'type' => $this -> determineIndexType ( $spec )
);
}
2011-01-12 03:05:32 +01:00
2012-09-24 05:36:59 +02:00
/**
* 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 postgres name of the index
*/
function buildMSSQLIndexName ( $tableName , $indexName , $prefix = 'ix' ) {
// Cleanup names of namespaced tables
$tableName = str_replace ( '\\' , '_' , $tableName );
2014-01-18 09:30:27 +01:00
$indexName = str_replace ( '\\' , '_' , $indexName );
2012-09-24 05:36:59 +02:00
return " { $prefix } _ { $tableName } _ { $indexName } " ;
2009-02-25 06:44:52 +01:00
}
2011-01-12 03:05:32 +01:00
2010-06-03 02:58:33 +02:00
/**
* Return SQL for dropping and recreating an index
*/
2009-02-25 06:44:52 +01:00
protected function getIndexSqlDefinition ( $tableName , $indexName , $indexSpec ) {
2012-06-26 04:52:18 +02:00
2012-09-24 05:36:59 +02:00
// Determine index name
$index = $this -> buildMSSQLIndexName ( $tableName , $indexName );
2011-10-13 03:12:58 +02:00
2012-09-24 05:36:59 +02:00
// Consolidate/Cleanup spec into array format
$indexSpec = $this -> parseIndexSpec ( $indexName , $indexSpec );
$drop = " IF EXISTS (SELECT name FROM sys.indexes WHERE name = ' $index ') DROP INDEX $index ON \" " . $tableName . " \" ; " ;
// create a type-specific index
if ( $indexSpec [ 'type' ] == 'fulltext' && $this -> fullTextEnabled ()) {
// 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; " ;
2009-10-22 08:51:35 +02:00
}
2012-09-24 05:36:59 +02:00
}
2011-01-12 03:05:32 +01:00
2012-09-24 05:36:59 +02:00
if ( $indexSpec [ 'type' ] == 'unique' ) {
return " $drop CREATE UNIQUE INDEX $index ON \" $tableName\ " ({ $indexSpec [ 'value' ]}); " ;
2009-02-25 06:44:52 +01:00
}
2012-09-24 05:36:59 +02:00
return " $drop CREATE INDEX $index ON \" $tableName\ " ({ $indexSpec [ 'value' ]}); " ;
2009-02-25 06:44:52 +01:00
}
2011-01-12 03:05:32 +01:00
2009-03-11 22:47:06 +01:00
function getDbSqlDefinition ( $tableName , $indexName , $indexSpec ){
return $indexName ;
}
2011-01-12 03:05:32 +01:00
2009-02-25 06:44:52 +01:00
/**
* Alter an index on a table .
* @ param string $tableName The name of the table .
* @ param string $indexName The name of the index .
2009-10-27 00:44:08 +01:00
* @ param string $indexSpec The specification of the index , see SS_Database :: requireIndex () for more details .
2009-02-25 06:44:52 +01:00
*/
public function alterIndex ( $tableName , $indexName , $indexSpec ) {
2012-09-24 05:36:59 +02:00
$this -> createIndex ( $tableName , $indexName , $indexSpec );
2009-02-25 06:44:52 +01:00
}
2011-01-12 03:05:32 +01:00
2009-02-25 06:44:52 +01:00
/**
* Return the list of indexes in a table .
* @ param string $table The table name .
* @ return array
*/
public function indexList ( $table ) {
2010-07-13 07:46:33 +02:00
$indexes = DB :: query ( " EXEC sp_helpindex ' $table '; " );
2009-06-15 08:41:36 +02:00
$indexList = array ();
2012-09-24 05:36:59 +02:00
// Enumerate all basic indexes
2009-03-17 04:58:58 +01:00
foreach ( $indexes as $index ) {
2010-07-13 07:46:33 +02:00
if ( strpos ( $index [ 'index_description' ], 'unique' ) !== false ) {
2013-01-07 04:25:31 +01:00
$prefix = 'unique ' ;
2012-09-24 05:36:59 +02:00
} else {
$prefix = 'index ' ;
2010-07-13 07:46:33 +02:00
}
2012-09-24 05:36:59 +02:00
// 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 ][ 'indexname' ] = $indexName ;
$indexList [ $indexName ][ 'spec' ] = " $prefix ( $columns ) " ;
2010-07-13 07:46:33 +02:00
}
2013-01-07 04:25:31 +01:00
// separately build up a list of the fulltext indexes for this table
// as MSSQL doesn't return fulltext indexes in sp_helpindex
2010-02-03 09:11:31 +01:00
if ( $this -> fullTextEnabled ()) {
2010-07-13 07:46:33 +02:00
$result = DB :: query ( 'EXEC sp_help_fulltext_columns;' );
2012-09-24 05:36:59 +02:00
// Extract columns from this fulltext definition
2013-01-07 04:25:31 +01:00
$columns = array ();
2010-07-13 07:46:33 +02:00
foreach ( $result as $row ) {
if ( $row [ 'TABLE_NAME' ] == $table ) {
2013-01-07 04:25:31 +01:00
$columns [] = $row [ 'FULLTEXT_COLUMN_NAME' ];
2010-07-13 07:46:33 +02:00
}
}
2012-09-24 05:36:59 +02:00
if ( ! empty ( $columns )) {
$columns = $this -> implodeColumnList ( $columns );
2010-07-13 07:46:33 +02:00
$indexList [ 'SearchFields' ][ 'indexname' ] = 'SearchFields' ;
2012-09-24 05:36:59 +02:00
$indexList [ 'SearchFields' ][ 'spec' ] = " fulltext ( $columns ) " ;
2010-07-13 07:46:33 +02:00
}
2009-06-15 08:41:36 +02:00
}
2009-02-25 06:44:52 +01:00
2010-07-13 07:46:33 +02:00
return $indexList ;
2009-02-25 06:44:52 +01:00
}
2013-01-07 04:25:31 +01:00
/**
* For a given table name , get all the internal index names ,
* except for those that are primary keys and fulltext indexes .
*
* @ return array
*/
public function indexNames ( $tableName ) {
return $this -> query ( sprintf ( ' 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 = \ '%s\'' , $tableName )) -> column ();
}
2009-02-25 06:44:52 +01:00
/**
* Returns a list of all the tables in the database .
* Table names will all be in lowercase .
* @ return array
*/
public function tableList () {
2009-05-07 05:47:59 +02:00
$tables = array ();
foreach ( $this -> query ( " EXEC sp_tables @table_owner = 'dbo'; " ) as $record ) {
2010-10-15 02:55:49 +02:00
$tables [ strtolower ( $record [ 'TABLE_NAME' ])] = $record [ 'TABLE_NAME' ];
2009-02-25 06:44:52 +01:00
}
2009-05-07 05:47:59 +02:00
return $tables ;
}
2011-01-12 03:05:32 +01:00
2009-05-07 05:47:59 +02:00
/**
2010-10-14 23:11:20 +02:00
* Empty the given table of all contents .
2009-05-07 05:47:59 +02:00
*/
public function clearTable ( $table ) {
$this -> query ( " TRUNCATE TABLE \" $table\ " " );
2009-02-25 06:44:52 +01:00
}
2011-01-12 03:05:32 +01:00
2009-02-25 06:44:52 +01:00
/**
* Return the number of rows affected by the previous operation .
* @ return int
*/
public function affectedRows () {
2009-05-28 04:07:11 +02:00
if ( $this -> mssql ) {
return mssql_rows_affected ( $this -> dbConn );
} else {
2010-03-11 09:47:04 +01:00
return $this -> lastAffectedRows ;
2009-05-28 04:07:11 +02:00
}
2009-02-25 06:44:52 +01:00
}
2010-03-11 09:41:37 +01:00
2009-02-25 06:44:52 +01:00
/**
* Return a boolean type - formatted string
2009-03-17 04:58:58 +01:00
* We use 'bit' so that we can do numeric - based comparisons
2011-01-12 03:05:32 +01:00
*
2009-02-25 06:44:52 +01:00
* @ params array $values Contains a tokenised list of info about this data type
* @ return string
*/
2010-02-03 08:50:29 +01:00
public function boolean ( $values ) {
2010-10-14 23:11:20 +02:00
$default = ( $values [ 'default' ]) ? '1' : '0' ;
2010-02-03 08:50:29 +01:00
return 'bit not null default ' . $default ;
2009-02-25 06:44:52 +01:00
}
2011-01-12 03:05:32 +01:00
2009-02-25 06:44:52 +01:00
/**
2009-04-28 07:13:23 +02:00
* Return a date type - formatted string .
2011-01-12 03:05:32 +01:00
*
2009-02-25 06:44:52 +01:00
* @ params array $values Contains a tokenised list of info about this data type
* @ return string
*/
2010-02-03 08:50:29 +01:00
public function date ( $values ) {
2012-06-12 02:55:27 +02:00
return 'date null' ;
2009-02-25 06:44:52 +01:00
}
2011-01-12 03:05:32 +01:00
2009-02-25 06:44:52 +01:00
/**
* Return a decimal type - formatted string
2011-01-12 03:05:32 +01:00
*
2009-02-25 06:44:52 +01:00
* @ params array $values Contains a tokenised list of info about this data type
* @ return string
*/
2010-02-03 08:50:29 +01:00
public function decimal ( $values ) {
2009-02-25 06:44:52 +01:00
// Avoid empty strings being put in the db
if ( $values [ 'precision' ] == '' ) {
$precision = 1 ;
} else {
$precision = $values [ 'precision' ];
}
2011-01-12 03:05:32 +01:00
2010-02-03 08:27:50 +01:00
$defaultValue = '0' ;
if ( isset ( $values [ 'default' ]) && is_numeric ( $values [ 'default' ])) {
$defaultValue = $values [ 'default' ];
}
2011-01-12 03:05:32 +01:00
2010-02-03 08:50:29 +01:00
return 'decimal(' . $precision . ') not null default ' . $defaultValue ;
2009-02-25 06:44:52 +01:00
}
2011-01-12 03:05:32 +01:00
2009-02-25 06:44:52 +01:00
/**
* Return a enum type - formatted string
2011-01-12 03:05:32 +01:00
*
2009-02-25 06:44:52 +01:00
* @ params array $values Contains a tokenised list of info about this data type
* @ return string
*/
2010-02-03 08:50:29 +01:00
public function enum ( $values ) {
2009-07-28 00:39:57 +02:00
// 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' ]));
2011-01-12 03:05:32 +01:00
return " varchar( $maxLength ) not null default ' " . $values [ 'default' ]
. " ' check( \" " . $values [ 'name' ] . " \" in (' " . implode ( " ',' " , $values [ 'enums' ])
2010-02-03 08:50:29 +01:00
. " ')) " ;
2009-02-25 06:44:52 +01:00
}
2011-01-12 03:05:32 +01:00
2010-02-07 02:59:13 +01:00
/**
* @ todo Make this work like { @ link MySQLDatabase :: set ()}
*/
public function set ( $values ) {
return $this -> enum ( $values );
}
2011-01-12 03:05:32 +01:00
2009-02-25 06:44:52 +01:00
/**
2009-04-28 07:13:23 +02:00
* Return a float type - formatted string .
2011-01-12 03:05:32 +01:00
*
2009-02-25 06:44:52 +01:00
* @ params array $values Contains a tokenised list of info about this data type
* @ return string
*/
2010-02-03 08:50:29 +01:00
public function float ( $values ) {
2013-04-25 01:28:05 +02:00
return 'float(53) not null default ' . $values [ 'default' ];
2009-02-25 06:44:52 +01:00
}
2011-01-12 03:05:32 +01:00
2009-02-25 06:44:52 +01:00
/**
* Return a int type - formatted string
2011-01-12 03:05:32 +01:00
*
2009-02-25 06:44:52 +01:00
* @ params array $values Contains a tokenised list of info about this data type
* @ return string
*/
2010-02-03 08:50:29 +01:00
public function int ( $values ) {
2009-02-25 06:44:52 +01:00
//We'll be using an 8 digit precision to keep it in line with the serial8 datatype for ID columns
2010-02-03 08:50:29 +01:00
return 'numeric(8) not null default ' . ( int ) $values [ 'default' ];
2009-02-25 06:44:52 +01:00
}
2011-01-12 03:05:32 +01:00
2009-02-25 06:44:52 +01:00
/**
* Return a datetime type - formatted string
* For MS SQL , we simply return the word 'timestamp' , no other parameters are necessary
2011-01-12 03:05:32 +01:00
*
2009-02-25 06:44:52 +01:00
* @ params array $values Contains a tokenised list of info about this data type
* @ return string
*/
2010-02-03 08:50:29 +01:00
public function ss_datetime ( $values ) {
return 'datetime null' ;
2009-02-25 06:44:52 +01:00
}
2011-01-12 03:05:32 +01:00
2009-02-25 06:44:52 +01:00
/**
* Return a text type - formatted string
2011-01-12 03:05:32 +01:00
*
2009-02-25 06:44:52 +01:00
* @ params array $values Contains a tokenised list of info about this data type
* @ return string
*/
2010-02-03 08:50:29 +01:00
public function text ( $values ) {
2010-07-13 07:07:40 +02:00
$collation = self :: $collation ? " COLLATE " . self :: $collation : " " ;
return " nvarchar(max) $collation null " ;
2009-02-25 06:44:52 +01:00
}
2011-01-12 03:05:32 +01:00
2009-02-25 06:44:52 +01:00
/**
2009-04-28 07:13:23 +02:00
* Return a time type - formatted string .
2011-01-12 03:05:32 +01:00
*
2009-02-25 06:44:52 +01:00
* @ params array $values Contains a tokenised list of info about this data type
* @ return string
*/
public function time ( $values ){
2009-05-16 03:33:45 +02:00
return 'time null' ;
2009-02-25 06:44:52 +01:00
}
2011-01-12 03:05:32 +01:00
2009-02-25 06:44:52 +01:00
/**
* Return a varchar type - formatted string
2011-01-12 03:05:32 +01:00
*
2009-02-25 06:44:52 +01:00
* @ params array $values Contains a tokenised list of info about this data type
* @ return string
*/
2010-02-03 08:50:29 +01:00
public function varchar ( $values ) {
2010-07-13 07:07:40 +02:00
$collation = self :: $collation ? " COLLATE " . self :: $collation : " " ;
return " nvarchar( " . $values [ 'precision' ] . " ) $collation null " ;
2009-02-25 06:44:52 +01:00
}
2011-01-12 03:05:32 +01:00
2009-04-28 07:13:23 +02:00
/**
* Return a 4 digit numeric type .
* @ return string
2009-02-25 06:44:52 +01:00
*/
2010-02-03 08:50:29 +01:00
public function year ( $values ) {
2011-01-12 03:05:32 +01:00
return 'numeric(4)' ;
2009-02-25 06:44:52 +01:00
}
2010-05-19 12:48:19 +02:00
2009-02-25 06:44:52 +01:00
/**
* 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
*/
2009-03-11 22:47:06 +01:00
function IdColumn ( $asDbValue = false , $hasAutoIncPK = true ){
2009-03-12 03:43:10 +01:00
if ( $asDbValue )
2009-07-28 05:48:52 +02:00
return 'bigint not null' ;
2009-03-12 03:43:10 +01:00
else {
if ( $hasAutoIncPK )
2009-07-28 05:48:52 +02:00
return 'bigint identity(1,1)' ;
else return 'bigint not null' ;
2009-03-12 03:43:10 +01:00
}
2009-02-25 06:44:52 +01:00
}
2011-01-12 03:05:32 +01:00
2009-03-12 00:04:01 +01:00
/**
* Returns the SQL command to get all the tables in this database
*/
function allTablesSQL (){
2010-05-19 13:06:35 +02:00
return " SELECT \" name \" FROM \" sys \" . \" tables \" ; " ;
2009-03-12 00:04:01 +01:00
}
2011-01-12 03:05:32 +01:00
2009-02-25 06:44:52 +01:00
/**
* Returns true if this table exists
* @ todo Make a proper implementation
*/
function hasTable ( $tableName ) {
2010-06-03 02:58:32 +02:00
$SQL_tableName = Convert :: raw2sql ( $tableName );
$value = DB :: query ( " SELECT table_name FROM information_schema.tables WHERE table_name = ' $SQL_tableName ' " ) -> value ();
return ( bool ) $value ;
2009-02-25 06:44:52 +01:00
}
2011-01-12 03:05:32 +01:00
2009-02-25 06:44:52 +01:00
/**
2009-04-06 00:23:58 +02:00
* Returns the values of the given enum field
* NOTE : Experimental ; introduced for db - abstraction and may changed before 2.4 is released .
2009-02-25 06:44:52 +01:00
*/
2009-04-06 00:23:58 +02:00
public function enumValuesForField ( $tableName , $fieldName ) {
2010-10-14 23:11:20 +02:00
$classes = array ();
2009-04-06 00:23:58 +02:00
// Get the enum of all page types from the SiteTree table
2010-10-14 23:11:20 +02:00
$clause = $this -> getConstraintCheckClause ( $tableName , $fieldName );
if ( $clause ) {
2010-10-15 00:36:55 +02:00
$classes = $this -> enumValuesFromCheckClause ( $clause );
2009-04-06 00:23:58 +02:00
}
2010-10-15 00:36:55 +02:00
2009-04-06 00:23:58 +02:00
return $classes ;
2009-02-25 06:44:52 +01:00
}
2009-03-11 22:47:06 +01:00
/**
2010-02-03 08:53:08 +01:00
* SQL Server uses CURRENT_TIMESTAMP for the current date / time .
2009-03-11 22:47:06 +01:00
*/
2010-02-03 08:53:08 +01:00
function now () {
2009-03-11 22:47:06 +01:00
return 'CURRENT_TIMESTAMP' ;
}
2011-01-12 03:05:32 +01:00
2009-09-17 09:07:02 +02:00
/**
* Returns the database - specific version of the random () function
*/
2011-01-12 03:05:32 +01:00
function random (){
2009-09-17 09:07:02 +02:00
return 'RAND()' ;
2010-04-07 04:46:28 +02:00
}
2011-01-12 03:05:32 +01:00
2010-04-07 04:46:28 +02:00
/**
* This is a lookup table for data types .
2011-01-12 03:05:32 +01:00
*
2010-04-07 04:46:28 +02:00
* 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 '' ;
2009-09-17 09:07:02 +02:00
}
2011-01-12 03:05:32 +01:00
2009-02-25 06:44:52 +01:00
/**
2009-04-28 07:13:23 +02:00
* Convert a SQLQuery object into a SQL statement .
2012-05-07 06:40:09 +02:00
*
* Needs to be overloaded from { @ link Database } because MSSQL has
* a very specific way of limiting results from a query .
*
* @ param SQLQuery
* @ return string SQL text
2009-02-25 06:44:52 +01:00
*/
2012-05-07 06:40:09 +02:00
public function sqlQueryToString ( SQLQuery $query ) {
2012-04-27 07:16:51 +02:00
// get the limit and offset
2012-05-07 06:40:09 +02:00
$limit = '' ;
$offset = '0' ;
$text = '' ;
$suffixText = '' ;
$nestedQuery = false ;
if ( is_array ( $query -> getLimit ())) {
$limitArr = $query -> getLimit ();
2012-05-08 07:41:01 +02:00
if ( isset ( $limitArr [ 'limit' ])) $limit = $limitArr [ 'limit' ];
2012-05-07 06:40:09 +02:00
if ( isset ( $limitArr [ 'start' ])) $offset = $limitArr [ 'start' ];
} else if ( preg_match ( '/^([0-9]+) offset ([0-9]+)$/i' , trim ( $query -> getLimit ()), $matches )) {
2009-06-08 05:45:54 +02:00
$limit = $matches [ 1 ];
$offset = $matches [ 2 ];
2009-03-23 01:51:28 +01:00
} else {
//could be a comma delimited string
2012-05-07 06:40:09 +02:00
$bits = explode ( ',' , $query -> getLimit ());
2009-06-16 06:04:01 +02:00
if ( sizeof ( $bits ) > 1 ) {
list ( $offset , $limit ) = $bits ;
} else {
$limit = $bits [ 0 ];
}
2009-03-23 01:51:28 +01:00
}
2011-01-12 03:05:32 +01:00
2009-06-16 04:51:42 +02:00
// DELETE queries
2012-05-07 06:40:09 +02:00
if ( $query -> getDelete ()) {
2009-03-23 01:51:28 +01:00
$text = 'DELETE ' ;
2009-06-16 04:51:42 +02:00
} else {
2012-05-07 06:40:09 +02:00
$distinct = $query -> getDistinct () ? 'DISTINCT ' : '' ;
2009-06-16 04:51:42 +02:00
// 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 ) {
2009-06-29 02:08:22 +02:00
$text = " SELECT $distinct TOP $limit " ;
2011-01-12 03:05:32 +01:00
2009-06-16 04:51:42 +02:00
// If there's a limit and an offset, then we need to do a subselect
} else if ( $limit && $offset ) {
2012-12-20 03:56:11 +01:00
$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 ) {
2012-12-20 04:04:44 +01:00
if ( preg_match ( '/_SortColumn/' , $field )) {
2012-12-20 03:56:11 +01:00
unset ( $orderby [ $field ]);
$orderby [ $selects [ str_replace ( '"' , '' , $field )]] = $dir ;
}
}
if ( $orderby ) {
$orderByClause = $this -> sqlOrderByToString ( $orderby );
2012-05-07 06:40:09 +02:00
$rowNumber = " ROW_NUMBER() OVER ( $orderByClause ) AS Number " ;
2009-06-16 06:04:01 +02:00
} else {
2012-05-07 06:40:09 +02:00
$selects = $query -> getSelect ();
2012-05-03 02:01:50 +02:00
$firstCol = reset ( $selects );
2009-06-16 06:04:01 +02:00
$rowNumber = " ROW_NUMBER() OVER (ORDER BY $firstCol ) AS Number " ;
}
2012-05-07 06:40:09 +02:00
$text = " SELECT * FROM (SELECT $distinct $rowNumber , " ;
2009-06-16 06:04:01 +02:00
$suffixText .= " ) AS Numbered WHERE Number BETWEEN " . ( $offset + 1 ) . " AND " . ( $offset + $limit )
2009-06-16 04:51:42 +02:00
. " ORDER BY Number " ;
$nestedQuery = true ;
// Otherwise a simple query
} else {
2009-06-16 07:32:31 +02:00
$text = " SELECT $distinct " ;
2009-06-16 04:51:42 +02:00
}
2011-01-12 03:05:32 +01:00
2009-06-16 04:51:42 +02:00
// Now add the columns to be selected
2012-05-07 06:40:09 +02:00
// strip off the SELECT text as it gets done above instead
$text .= trim ( str_replace ( 'SELECT' , '' , $this -> sqlSelectToString ( $query -> getSelect ())));
2009-02-25 06:44:52 +01:00
}
2012-05-07 06:40:09 +02:00
if ( $query -> getFrom ()) $text .= $this -> sqlFromToString ( $query -> getFrom ());
if ( $query -> getWhere ()) $text .= $this -> sqlWhereToString ( $query -> getWhere (), $query -> getConnective ());
2012-04-27 07:16:51 +02:00
2012-05-07 06:40:09 +02:00
// these clauses only make sense in SELECT queries, not DELETE
if ( ! $query -> getDelete ()) {
if ( $query -> getGroupBy ()) $text .= $this -> sqlGroupByToString ( $query -> getGroupBy ());
if ( $query -> getHaving ()) $text .= $this -> sqlHavingToString ( $query -> getHaving ());
if ( $query -> getOrderBy () && ! $nestedQuery ) $text .= $this -> sqlOrderByToString ( $query -> getOrderBy ());
2012-04-27 07:16:51 +02:00
}
2011-01-12 03:05:32 +01:00
2009-06-16 04:51:42 +02:00
// $suffixText is used by the nested queries to create an offset limit
if ( $suffixText ) $text .= $suffixText ;
2011-01-12 03:05:32 +01:00
2009-02-25 06:44:52 +01:00
return $text ;
}
2009-03-11 22:47:06 +01:00
2010-12-16 00:18:27 +01:00
/**
* Escapes a value with specific escape characters specific to the MSSQL .
* @ param string $value String to escape
* @ return string Escaped string
*/
2011-01-12 03:05:32 +01:00
function addslashes ( $value ){
$value = str_replace ( " ' " , " '' " , $value );
$value = str_replace ( " \0 " , " [NULL] " , $value );
return $value ;
2009-03-11 22:47:06 +01:00
}
2010-12-16 00:18:27 +01:00
2010-05-19 13:06:35 +02:00
/**
2009-03-17 20:27:15 +01:00
* This changes the index name depending on database requirements .
*/
2010-05-19 13:06:35 +02:00
function modifyIndex ( $index ) {
2012-09-24 05:36:59 +02:00
return $index ;
2009-03-17 20:27:15 +01:00
}
2010-12-16 00:18:27 +01:00
2009-03-23 01:51:28 +01:00
/**
2009-07-20 11:09:53 +02:00
* The core search engine configuration .
2010-10-20 02:52:44 +02:00
* 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 .
2011-01-12 03:05:32 +01:00
*
2010-10-20 02:52:44 +02:00
* @ param array classesToSearch computes all descendants and includes them . Check is done via WHERE clause .
2009-07-20 11:09:53 +02:00
* @ param string $keywords Keywords as a space separated string
* @ return object DataObjectSet of result pages
2009-03-23 01:51:28 +01:00
*/
2009-06-05 06:37:45 +02:00
public function searchEngine ( $classesToSearch , $keywords , $start , $pageLength , $sortBy = " Relevance DESC " , $extraFilter = " " , $booleanSearch = false , $alternativeFileFilter = " " , $invertedMatch = false ) {
2012-05-03 04:33:17 +02:00
if ( isset ( $objects )) $results = new ArrayList ( $objects );
else $results = new ArrayList ();
2011-12-17 00:43:09 +01:00
if ( ! $this -> fullTextEnabled ()) return $results ;
2010-11-01 04:20:52 +01:00
if ( ! in_array ( substr ( $sortBy , 0 , 9 ), array ( '"Relevanc' , 'Relevance' ))) user_error ( " Non-relevance sort not supported. " , E_USER_ERROR );
2010-10-20 02:52:44 +02:00
$allClassesToSearch = array ();
foreach ( $classesToSearch as $class ) {
$allClassesToSearch = array_merge ( $allClassesToSearch , ClassInfo :: dataClassesFor ( $class ));
}
$allClassesToSearch = array_unique ( $allClassesToSearch );
2011-01-12 03:05:32 +01:00
2009-10-27 08:08:51 +01:00
//Get a list of all the tables and columns we'll be searching on:
2010-08-31 07:52:48 +02:00
$fulltextColumns = DB :: query ( 'EXEC sp_help_fulltext_columns' );
$queries = array ();
// Sort the columns back into tables.
2010-05-19 14:11:33 +02:00
$tables = array ();
2010-08-31 07:52:48 +02:00
foreach ( $fulltextColumns as $column ) {
// Skip extension tables.
if ( substr ( $column [ 'TABLE_NAME' ], - 5 ) == '_Live' || substr ( $column [ 'TABLE_NAME' ], - 9 ) == '_versions' ) continue ;
// Add the column to table.
$table = & $tables [ $column [ 'TABLE_NAME' ]];
if ( ! $table ) $table = array ( $column [ 'FULLTEXT_COLUMN_NAME' ]);
else array_push ( $table , $column [ 'FULLTEXT_COLUMN_NAME' ]);
}
2010-10-20 02:52:44 +02:00
// Create one query per each table, $columns not used. We want just the ID and the ClassName of the object from this query.
2010-08-31 07:52:48 +02:00
foreach ( $tables as $tableName => $columns ){
2010-10-20 02:52:44 +02:00
$baseClass = ClassInfo :: baseDataClass ( $tableName );
2010-08-31 07:52:48 +02:00
$join = $this -> fullTextSearchMSSQL ( $tableName , $keywords );
2011-12-17 00:43:09 +01:00
if ( ! $join ) return $results ; // avoid "Null or empty full-text predicate"
2010-08-31 07:52:48 +02:00
// Check if we need to add ShowInSearch
$where = null ;
if ( strpos ( $tableName , 'SiteTree' ) === 0 ) {
$where = array ( " \" $tableName\ " . \ " ShowInSearch \" !=0 " );
2011-09-15 16:02:23 +02:00
} elseif ( strpos ( $tableName , 'File' ) === 0 ) {
2011-12-17 00:43:09 +01:00
// File.ShowInSearch was added later, keep the database driver backwards compatible
2011-09-15 16:02:23 +02:00
// by checking for its existence first
$fields = $this -> fieldList ( $tableName );
if ( array_key_exists ( 'ShowInSearch' , $fields )) {
$where = array ( " \" $tableName\ " . \ " ShowInSearch \" !=0 " );
}
2009-05-08 05:37:31 +02:00
}
2010-08-31 07:52:48 +02:00
2012-05-03 04:33:17 +02:00
$queries [ $tableName ] = DataList :: create ( $tableName ) -> where ( $where , '' ) -> dataQuery () -> query ();
2012-05-07 06:40:09 +02:00
$queries [ $tableName ] -> setOrderBy ( array ());
2010-10-20 02:52:44 +02:00
// Join with CONTAINSTABLE, a full text searcher that includes relevance factor
2012-05-07 06:40:09 +02:00
$queries [ $tableName ] -> setFrom ( array ( " \" $tableName\ " INNER JOIN $join AS \ " ft \" ON \" $tableName\ " . \ " ID \" = \" ft \" . \" KEY \" " ));
2010-10-20 02:52:44 +02:00
// Join with the base class if needed, as we want to test agains the ClassName
2011-12-17 00:43:09 +01:00
if ( $tableName != $baseClass ) {
2012-05-07 06:40:09 +02:00
$queries [ $tableName ] -> setFrom ( " INNER JOIN \" $baseClass\ " ON \ " $baseClass\ " . \ " ID \" = \" $tableName\ " . \ " ID \" " );
2010-10-20 02:52:44 +02:00
}
2012-05-03 04:33:17 +02:00
2012-05-07 06:40:09 +02:00
$queries [ $tableName ] -> setSelect ( array ( " \" $tableName\ " . \ " ID \" " ));
2012-05-03 04:33:17 +02:00
$queries [ $tableName ] -> selectField ( " ' $tableName ' " , 'Source' );
$queries [ $tableName ] -> selectField ( 'Rank' , 'Relevance' );
2010-10-20 02:52:44 +02:00
if ( $extraFilter ) {
2012-05-07 06:40:09 +02:00
$queries [ $tableName ] -> addWhere ( $extraFilter );
2010-10-20 02:52:44 +02:00
}
if ( count ( $allClassesToSearch )) {
2012-05-07 06:40:09 +02:00
$queries [ $tableName ] -> addWhere ( " \" $baseClass\ " . \ " ClassName \" IN (' " . implode ( $allClassesToSearch , " ', ' " ) . " ') " );
2010-10-20 02:52:44 +02:00
}
// Reset the parameters that would get in the way
2010-08-31 07:52:48 +02:00
}
2010-09-01 06:06:36 +02:00
// Generate SQL
2010-08-31 07:52:48 +02:00
$querySQLs = array ();
foreach ( $queries as $query ) {
$querySQLs [] = $query -> sql ();
2009-03-23 01:51:28 +01:00
}
2010-05-19 14:11:33 +02:00
2010-08-31 07:52:48 +02:00
// Unite the SQL
$fullQuery = implode ( " UNION " , $querySQLs ) . " ORDER BY $sortBy " ;
2010-05-19 14:11:33 +02:00
2010-08-31 07:52:48 +02:00
// Perform the search
$result = DB :: query ( $fullQuery );
2010-09-01 06:49:07 +02:00
// Regenerate DataObjectSet - watch out, numRecords doesn't work on sqlsrv driver on Windows.
2010-09-01 06:06:36 +02:00
$current = - 1 ;
2011-12-17 00:43:09 +01:00
$objects = array ();
2010-09-01 06:06:36 +02:00
foreach ( $result as $row ) {
$current ++ ;
2013-05-27 05:21:24 +02:00
// Select a subset for paging
if ( $current >= $start && $current < $start + $pageLength ) {
$objects [] = DataObject :: get_by_id ( $row [ 'Source' ], $row [ 'ID' ]);
}
2010-09-01 06:06:36 +02:00
}
2010-05-19 14:11:33 +02:00
2012-05-07 06:40:09 +02:00
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 );
2013-05-27 05:21:24 +02:00
// The list has already been limited by the query above
$list -> setLimitItems ( false );
2012-05-07 06:40:09 +02:00
return $list ;
2009-03-23 01:51:28 +01:00
}
2010-05-19 14:11:33 +02:00
2009-05-07 07:55:05 +02:00
/**
* Allow auto - increment primary key editing on the given table .
* 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
*/
function allowPrimaryKeyEditing ( $table , $allow = true ) {
$this -> query ( " SET IDENTITY_INSERT \" $table\ " " . ( $allow ? " ON " : " OFF " ));
}
2009-05-07 08:32:08 +02:00
2009-12-07 06:38:26 +01:00
/**
* Check if a fulltext index exists on a particular table name .
2010-02-02 00:24:02 +01:00
* @ return boolean TRUE index exists | FALSE index does not exist | NULL no support
2009-12-07 06:38:26 +01:00
*/
function fulltextIndexExists ( $tableName ) {
2010-02-02 00:24:02 +01:00
// Special case for no full text index support
2010-02-03 09:11:31 +01:00
if ( ! $this -> fullTextEnabled ()) return null ;
2011-10-13 03:12:58 +02:00
2009-12-07 06:38:26 +01:00
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();
}
2009-05-07 08:32:08 +02:00
/**
* Returns a SQL fragment for querying a fulltext search index
2010-08-31 07:52:48 +02:00
*
* @ param $tableName specific - table name
2009-05-07 08:32:08 +02:00
* @ param $keywords string The search query
2010-08-31 07:52:48 +02:00
* @ param $fields array The list of field names to search on , or null to include all
2010-09-01 00:19:58 +02:00
*
* @ returns null if keyword set is empty or the string with JOIN clause to be added to SQL query
2009-05-07 08:32:08 +02:00
*/
2010-08-31 07:52:48 +02:00
function fullTextSearchMSSQL ( $tableName , $keywords , $fields = null ) {
// Make sure we are getting an array of fields
if ( isset ( $fields ) && ! is_array ( $fields )) $fields = array ( $fields );
// Strip unfriendly characters, SQLServer "CONTAINS" predicate will crash on & and | and ignore others anyway.
if ( function_exists ( 'mb_ereg_replace' )) {
$keywords = mb_ereg_replace ( '[^\w\s]' , '' , trim ( $keywords ));
}
else {
$keywords = Convert :: raw2sql ( str_replace ( array ( '&' , '|' , '!' , '"' , '\'' ), '' , trim ( $keywords )));
}
2011-01-12 03:05:32 +01:00
2010-08-31 07:52:48 +02:00
// Remove stopwords, concat with ANDs
$keywords = explode ( ' ' , $keywords );
$keywords = self :: removeStopwords ( $keywords );
2009-05-07 08:32:08 +02:00
2012-12-18 23:19:21 +01:00
// remove any empty values from the array
foreach ( $keywords as $i => $word ) {
if ( ! strlen ( $word )) unset ( $keywords [ $i ]);
}
$keywords = implode ( ' AND ' , $keywords );
2010-09-01 00:19:58 +02:00
if ( ! $keywords || trim ( $keywords ) == '' ) return null ;
2010-08-31 07:52:48 +02:00
if ( $fields ) $fieldNames = '"' . implode ( '", "' , $fields ) . '"' ;
else $fieldNames = " * " ;
2009-05-07 08:32:08 +02:00
2012-04-26 07:31:12 +02:00
return " CONTAINSTABLE( \" $tableName\ " , ( $fieldNames ), '$keywords' ) " ;
2009-05-07 08:32:08 +02:00
}
2011-01-12 03:05:32 +01:00
2009-07-09 03:11:02 +02:00
/**
2010-08-31 07:52:48 +02:00
* Remove stopwords that would kill a MSSQL full - text query
2009-07-09 03:11:02 +02:00
*
2011-01-12 03:05:32 +01:00
* @ param array $keywords
2010-08-31 07:52:48 +02:00
*
* @ return array $keywords with stopwords removed
*/
static public function removeStopwords ( $keywords ) {
$goodKeywords = array ();
foreach ( $keywords as $keyword ) {
if ( in_array ( $keyword , self :: $noiseWords )) continue ;
$goodKeywords [] = trim ( $keyword );
2009-07-09 03:11:02 +02:00
}
2010-08-31 07:52:48 +02:00
return $goodKeywords ;
2009-07-09 03:11:02 +02:00
}
2011-01-12 03:05:32 +01:00
2010-10-02 05:36:04 +02:00
/**
2009-10-01 23:11:18 +02:00
* Does this database support transactions ?
*/
public function supportsTransactions (){
return $this -> supportsTransactions ;
}
2011-01-12 03:05:32 +01:00
2010-10-02 05:36:04 +02:00
/**
2009-10-12 00:04:24 +02:00
* This is a quick lookup to discover if the database supports particular extensions
* Currently , MSSQL supports no extensions
*/
public function supportsExtensions ( $extensions = Array ( 'partitions' , 'tablespaces' , 'clustering' )){
if ( isset ( $extensions [ 'partitions' ]))
return false ;
elseif ( isset ( $extensions [ 'tablespaces' ]))
return false ;
elseif ( isset ( $extensions [ 'clustering' ]))
return false ;
else
return false ;
}
2011-03-11 04:41:14 +01:00
2010-10-02 05:36:04 +02:00
/**
2010-08-31 05:18:00 +02:00
* Start transaction . READ ONLY not supported .
2009-10-01 23:11:18 +02:00
*/
2011-03-11 04:41:14 +01:00
public function transactionStart ( $transaction_mode = false , $session_characteristics = false ){
2010-08-31 05:18:00 +02:00
if ( $this -> mssql ) {
DB :: query ( 'BEGIN TRANSACTION' );
} else {
$result = sqlsrv_begin_transaction ( $this -> dbConn );
if ( ! $result ) $this -> databaseError ( " Couldn't start the transaction. " , E_USER_ERROR );
}
2009-10-01 23:11:18 +02:00
}
2011-01-12 03:05:32 +01:00
2010-10-02 05:36:04 +02:00
/**
2009-10-01 23:11:18 +02:00
* Create a savepoint that you can jump back to if you encounter problems
*/
public function transactionSavepoint ( $savepoint ){
2010-10-02 05:36:04 +02:00
DB :: query ( " SAVE TRANSACTION \" $savepoint\ " " );
2009-10-01 23:11:18 +02:00
}
2011-01-12 03:05:32 +01:00
2010-10-02 05:36:04 +02:00
/**
2009-10-01 23:11:18 +02:00
* 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 ( $savepoint = false ){
2010-06-24 07:37:45 +02:00
if ( $savepoint ) {
2010-10-02 05:36:04 +02:00
DB :: query ( " ROLLBACK TRANSACTION \" $savepoint\ " " );
2010-06-24 07:37:45 +02:00
} else {
2010-08-31 05:18:00 +02:00
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 );
}
2010-06-24 07:37:45 +02:00
}
2009-10-01 23:11:18 +02:00
}
2011-03-11 04:41:14 +01:00
2010-10-02 05:36:04 +02:00
/**
2009-10-01 23:11:18 +02:00
* Commit everything inside this transaction so far
*/
2011-03-11 04:41:14 +01:00
public function transactionEnd (){
2010-08-31 05:18:00 +02:00
if ( $this -> mssql ) {
DB :: query ( 'COMMIT TRANSACTION' );
} else {
$result = sqlsrv_commit ( $this -> dbConn );
if ( ! $result ) $this -> databaseError ( " Couldn't commit the transaction. " , E_USER_ERROR );
}
2009-10-01 23:11:18 +02:00
}
2010-02-03 06:01:37 +01:00
2012-03-21 00:03:11 +01:00
/**
* Overload the Database :: prepStringForDB () method and include " N " prefix so unicode
* strings are saved to the database correctly .
*
* @ param string $string String to be encoded
* @ return string Processed string ready for DB
*/
public function prepStringForDB ( $string ) {
return " N' " . Convert :: raw2sql ( $string ) . " ' " ;
}
2012-12-11 01:47:35 +01:00
/**
* Generate a WHERE clause for text matching .
*
* @ param String $field Quoted field name
* @ param String $value Escaped search . Can include percentage wildcards .
* @ param boolean $exact Exact matches or wildcard support .
* @ param boolean $negate Negate the clause .
* @ param boolean $caseSensitive Enforce case sensitivity if TRUE or FALSE .
* Stick with default collation if set to NULL .
* @ return String SQL
*/
public function comparisonClause ( $field , $value , $exact = false , $negate = false , $caseSensitive = null ) {
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 :: $collation ) {
$collation = preg_replace ( '/_CI_/' , '_CS_' , self :: $collation );
} else {
$collation = 'Latin1_General_CS_AS' ;
}
$collateClause = ' COLLATE ' . $collation ;
} elseif ( $caseSensitive === false ) {
if ( self :: $collation ) {
$collation = preg_replace ( '/_CS_/' , '_CI_' , self :: $collation );
} else {
$collation = 'Latin1_General_CI_AS' ;
}
$collateClause = ' COLLATE ' . $collation ;
}
$clause = sprintf ( " %s %s '%s' " , $field , $comp , $value );
if ( $collateClause ) $clause .= $collateClause ;
return $clause ;
}
2010-02-03 06:01:37 +01:00
/**
2010-05-18 12:47:05 +02:00
* Function to return an SQL datetime expression for MSSQL
2010-02-03 06:01:37 +01:00
* 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 $format to be used , supported specifiers :
* % Y = Year ( four digits )
* % m = Month ( 01. . 12 )
* % d = Day ( 01. . 31 )
* % H = Hour ( 00. . 23 )
* % i = Minutes ( 00. . 59 )
* % s = Seconds ( 00. . 59 )
* % U = unix timestamp , can only be used on it ' s own
* @ return string SQL datetime expression to query for a formatted datetime
*/
function formattedDatetimeClause ( $date , $format ) {
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 );
if ( preg_match ( '/^now$/i' , $date )) {
$date = " CURRENT_TIMESTAMP " ;
} else if ( preg_match ( '/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i' , $date )) {
2010-02-04 05:53:25 +01:00
$date = " ' $date .000' " ;
2010-02-03 06:01:37 +01:00
}
2010-09-29 12:10:51 +02:00
if ( $format == '%U' ) {
return " DATEDIFF(s, '1970-01-01 00:00:00', DATEADD(hour, DATEDIFF(hour, GETDATE(), GETUTCDATE()), $date )) " ;
}
2010-02-03 06:01:37 +01:00
$trans = array (
'Y' => 'yy' ,
'm' => 'mm' ,
'd' => 'dd' ,
'H' => 'hh' ,
'i' => 'mi' ,
's' => 'ss' ,
);
$strings = array ();
$buffer = $format ;
while ( strlen ( $buffer )) {
if ( substr ( $buffer , 0 , 1 ) == '%' ) {
$f = substr ( $buffer , 1 , 1 );
$flen = $f == 'Y' ? 4 : 2 ;
$strings [] = " RIGHT('0' + CAST(DATEPART( { $trans [ $f ] } , $date ) AS VARCHAR), $flen ) " ;
$buffer = substr ( $buffer , 2 );
} else {
$pos = strpos ( $buffer , '%' );
if ( $pos === false ) {
$strings [] = $buffer ;
$buffer = '' ;
} else {
$strings [] = " ' " . substr ( $buffer , 0 , $pos ) . " ' " ;
$buffer = substr ( $buffer , $pos );
}
}
}
return '(' . implode ( ' + ' , $strings ) . ')' ;
2011-01-12 03:05:32 +01:00
2010-02-03 06:01:37 +01:00
}
2011-01-12 03:05:32 +01:00
2010-02-03 06:01:37 +01:00
/**
2010-05-18 12:47:05 +02:00
* Function to return an SQL datetime expression for MSSQL .
2010-02-03 06:01:37 +01:00
* 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 $interval to be added , use the format [ sign ][ integer ] [ qualifier ], e . g . - 1 Day , + 15 minutes , + 1 YEAR
* supported qualifiers :
* - years
* - months
* - days
* - hours
* - minutes
* - seconds
* 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
*/
function datetimeIntervalClause ( $date , $interval ) {
$trans = array (
'year' => 'yy' ,
'month' => 'mm' ,
'day' => 'dd' ,
'hour' => 'hh' ,
'minute' => 'mi' ,
'second' => 'ss' ,
);
$singularinterval = preg_replace ( '/(year|month|day|hour|minute|second)s/i' , '$1' , $interval );
if (
! ( $params = preg_match ( '/([-+]\d+) (\w+)/i' , $singularinterval , $matches )) ||
! isset ( $trans [ strtolower ( $matches [ 2 ])])
) user_error ( 'datetimeIntervalClause(): invalid interval ' . $interval , E_USER_WARNING );
if ( preg_match ( '/^now$/i' , $date )) {
$date = " CURRENT_TIMESTAMP " ;
} else if ( preg_match ( '/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i' , $date )) {
$date = " ' $date ' " ;
}
2010-05-19 12:09:51 +02:00
return " CONVERT(VARCHAR, DATEADD( " . $trans [ strtolower ( $matches [ 2 ])] . " , " . ( int ) $matches [ 1 ] . " , $date ), 120) " ;
2010-02-03 06:01:37 +01:00
}
/**
2010-05-18 12:47:05 +02:00
* Function to return an SQL datetime expression for MSSQL .
2010-02-03 06:01:37 +01:00
* 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 $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
*/
function datetimeDifferenceClause ( $date1 , $date2 ) {
if ( preg_match ( '/^now$/i' , $date1 )) {
$date1 = " CURRENT_TIMESTAMP " ;
} else if ( preg_match ( '/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i' , $date1 )) {
$date1 = " ' $date1 ' " ;
}
if ( preg_match ( '/^now$/i' , $date2 )) {
$date2 = " CURRENT_TIMESTAMP " ;
} else if ( preg_match ( '/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i' , $date2 )) {
$date2 = " ' $date2 ' " ;
}
return " DATEDIFF(s, $date2 , $date1 ) " ;
}
2009-02-25 06:44:52 +01:00
}
/**
2009-04-28 07:13:23 +02:00
* A result - set from a MSSQL database .
2009-02-25 06:44:52 +01:00
* @ package sapphire
* @ subpackage model
*/
2009-10-26 21:59:29 +01:00
class MSSQLQuery extends SS_Query {
2010-05-19 13:37:43 +02:00
2009-02-25 06:44:52 +01:00
/**
2009-04-28 07:13:23 +02:00
* The MSSQLDatabase object that created this result set .
* @ var MSSQLDatabase
2009-02-25 06:44:52 +01:00
*/
private $database ;
2010-05-19 13:37:43 +02:00
2009-02-25 06:44:52 +01:00
/**
2009-04-28 07:13:23 +02:00
* The internal MSSQL handle that points to the result set .
2009-02-25 06:44:52 +01:00
* @ var resource
*/
private $handle ;
2010-05-19 13:37:43 +02:00
2009-05-28 04:07:11 +02:00
/**
* If true , use the mssql_ ... functions .
* If false use the sqlsrv_ ... functions
*/
private $mssql = null ;
2009-02-25 06:44:52 +01:00
2012-06-12 03:31:29 +02:00
/**
* A list of field meta - data , such as column name and data type .
* @ var array
*/
private $fields = array ();
2009-02-25 06:44:52 +01:00
/**
* Hook the result - set given into a Query class , suitable for use by sapphire .
* @ param database The database object that created this query .
2009-04-28 07:13:23 +02:00
* @ param handle the internal mssql handle that is points to the resultset .
2009-02-25 06:44:52 +01:00
*/
2009-05-28 04:07:11 +02:00
public function __construct ( MSSQLDatabase $database , $handle , $mssql ) {
2009-02-25 06:44:52 +01:00
$this -> database = $database ;
$this -> handle = $handle ;
2009-05-28 04:07:11 +02:00
$this -> mssql = $mssql ;
2012-06-12 03:31:29 +02:00
// build a list of field meta-data for this query we'll use in nextRecord()
// doing it here once saves us from calling mssql_fetch_field() in nextRecord()
// potentially hundreds of times, which is unnecessary.
if ( $this -> mssql && is_resource ( $this -> handle )) {
for ( $i = 0 ; $i < mssql_num_fields ( $handle ); $i ++ ) {
$this -> fields [ $i ] = mssql_fetch_field ( $handle , $i );
}
}
2009-02-25 06:44:52 +01:00
}
2010-05-19 13:37:43 +02:00
2010-10-14 07:03:01 +02:00
public function __destruct () {
if ( is_resource ( $this -> handle )) {
2012-05-03 04:33:17 +02:00
if ( $this -> mssql ) {
mssql_free_result ( $this -> handle );
} else {
sqlsrv_free_stmt ( $this -> handle );
}
2009-05-28 04:07:11 +02:00
}
2009-02-25 06:44:52 +01:00
}
2009-10-27 00:44:08 +01:00
2009-02-25 06:44:52 +01:00
public function seek ( $row ) {
2010-10-05 02:55:16 +02:00
if ( ! is_resource ( $this -> handle )) return false ;
2009-05-28 04:07:11 +02:00
if ( $this -> mssql ) {
return mssql_data_seek ( $this -> handle , $row );
} else {
2010-10-05 02:55:16 +02:00
user_error ( 'MSSQLQuery::seek() not supported in sqlsrv' , E_USER_WARNING );
2009-05-28 04:07:11 +02:00
}
2009-02-25 06:44:52 +01:00
}
2010-05-19 13:37:43 +02:00
2009-02-25 06:44:52 +01:00
public function numRecords () {
2010-06-24 07:37:45 +02:00
if ( ! is_resource ( $this -> handle )) return false ;
2012-05-03 04:33:17 +02:00
2009-05-28 04:07:11 +02:00
if ( $this -> mssql ) {
return mssql_num_rows ( $this -> handle );
} else {
2010-10-05 02:55:16 +02:00
// WARNING: This will only work if the cursor type is scrollable!
2010-05-19 14:13:33 +02:00
if ( function_exists ( 'sqlsrv_num_rows' )) {
return sqlsrv_num_rows ( $this -> handle );
2010-09-01 06:49:07 +02:00
} else {
2010-10-05 02:55:16 +02:00
user_error ( 'MSSQLQuery::numRecords() not supported in this version of sqlsrv' , E_USER_WARNING );
2010-05-19 14:13:33 +02:00
}
2009-05-28 04:07:11 +02:00
}
2009-02-25 06:44:52 +01:00
}
2010-05-19 13:37:43 +02:00
2009-02-25 06:44:52 +01:00
public function nextRecord () {
2010-06-24 07:37:45 +02:00
if ( ! is_resource ( $this -> handle )) return false ;
2011-01-12 03:05:32 +01:00
if ( $this -> mssql ) {
2012-06-12 02:55:27 +02:00
if ( $row = mssql_fetch_row ( $this -> handle )) {
foreach ( $row as $i => $value ) {
2012-06-12 03:31:29 +02:00
$field = $this -> fields [ $i ];
2012-06-12 02:55:27 +02:00
// fix datetime formatting from format "Jan 1 2012 12:00:00:000AM" to "2012-01-01 12:00:00"
// strtotime doesn't understand this format, so we need to do some modification of the value first
if ( $field -> type == 'datetime' && $value ) {
$value = date ( 'Y-m-d H:i:s' , strtotime ( preg_replace ( '/:[0-9][0-9][0-9]([ap]m)$/i' , ' \\1' , $value )));
}
if ( isset ( $value ) || ! isset ( $data [ $field -> name ])) {
$data [ $field -> name ] = $value ;
}
}
2012-05-03 04:33:17 +02:00
return $data ;
2013-05-22 12:38:05 +02:00
} else {
// free the handle, no more results
mssql_free_result ( $this -> handle );
$this -> handle = null ;
2009-02-25 06:44:52 +01:00
}
} else {
2012-05-03 04:33:17 +02:00
if ( $data = sqlsrv_fetch_array ( $this -> handle , SQLSRV_FETCH_ASSOC )) {
2012-05-04 11:58:48 +02:00
// 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' );
}
2012-05-03 04:33:17 +02:00
return $data ;
2009-05-28 04:07:11 +02:00
} else {
2010-05-19 13:37:43 +02:00
// Free the handle if there are no more results - sqlsrv crashes if there are too many handles
2012-05-03 02:01:50 +02:00
sqlsrv_free_stmt ( $this -> handle );
$this -> handle = null ;
2009-05-28 04:07:11 +02:00
}
2009-02-25 06:44:52 +01:00
}
2010-05-19 13:37:43 +02:00
return false ;
2009-02-25 06:44:52 +01:00
}
2010-05-19 13:37:43 +02:00
2010-08-31 07:52:48 +02:00
}