2010-01-14 21:42:43 +01:00
< ? php
/**
2013-04-03 06:49:59 +02:00
* SQLite database controller class
*
2010-01-21 12:32:21 +01:00
* @ package SQLite3
2010-01-14 21:42:43 +01:00
*/
class SQLite3Database extends SS_Database {
2013-04-03 06:49:59 +02:00
2010-01-17 11:48:22 +01:00
/**
2013-04-03 06:49:59 +02:00
* Database schema manager object
*
* @ var SQLite3SchemaManager
2010-01-17 11:48:22 +01:00
*/
2013-04-03 06:49:59 +02:00
protected $schemaManager = null ;
2010-01-17 11:48:22 +01:00
2013-04-03 06:49:59 +02:00
/*
* This holds the parameters that the original connection was created with ,
* so we can switch back to it if necessary ( used for unit tests )
*
* @ var array
*/
protected $parameters ;
/*
* if we ' re on a In - Memory db
*
2010-01-17 11:48:22 +01:00
* @ var boolean
*/
2013-04-03 06:49:59 +02:00
protected $livesInMemory = false ;
2010-01-17 11:48:22 +01:00
/**
2013-04-03 06:49:59 +02:00
* List of default pragma values
*
* @ todo Migrate to SS config
*
* @ var array
2010-01-17 11:48:22 +01:00
*/
2013-04-03 06:49:59 +02:00
public static $default_pragma = array (
'encoding' => '"UTF-8"' ,
'locking_mode' => 'NORMAL'
);
2010-01-17 11:48:22 +01:00
2013-04-03 06:49:59 +02:00
/**
* Extension used to distinguish between sqllite database files and other files .
* Required to handle multiple databases .
*
* @ return string
2010-01-17 11:48:22 +01:00
*/
2013-04-03 06:49:59 +02:00
public static function database_extension () {
return Config :: inst () -> get ( 'SQLite3Database' , 'database_extension' );
}
2010-01-17 11:48:22 +01:00
2013-04-03 06:49:59 +02:00
/**
* Check if a database name has a valid extension
*
* @ param string $name
* @ return boolean
*/
public static function is_valid_database_name ( $name ) {
$extension = self :: database_extension ();
if ( empty ( $extension )) return true ;
2010-01-24 07:57:42 +01:00
2013-04-03 06:49:59 +02:00
return substr_compare ( $name , $extension , - strlen ( $extension ), strlen ( $extension )) === 0 ;
}
2010-05-20 23:43:00 +02:00
2010-01-17 11:48:22 +01:00
/**
* Connect to a SQLite3 database .
* @ param array $parameters An map of parameters , which should include :
2013-04-03 06:49:59 +02:00
* - database : The database to connect to , with the correct file extension ( . sqlite )
2010-01-17 11:48:22 +01:00
* - path : the path to the SQLite3 database file
* - key : the encryption key ( needs testing )
* - memory : use the faster In - Memory database for unit tests
*/
2013-04-03 06:49:59 +02:00
public function connect ( $parameters ) {
2014-03-05 22:13:12 +01:00
if ( ! empty ( $parameters [ 'memory' ])) {
Deprecation :: notice (
'1.4.0' ,
" \$ databaseConfig['memory'] is deprecated. Use \$ databaseConfig['path'] = ':memory:' instead. " ,
Deprecation :: SCOPE_GLOBAL
);
2013-04-03 06:49:59 +02:00
unset ( $parameters [ 'memory' ]);
2014-03-05 22:13:12 +01:00
$parameters [ 'path' ] = ':memory:' ;
}
2013-04-03 06:49:59 +02:00
//We will store these connection parameters for use elsewhere (ie, unit tests)
$this -> parameters = $parameters ;
$this -> schemaManager -> flushCache ();
2010-01-17 11:48:22 +01:00
2013-04-03 06:49:59 +02:00
// Ensure database name is set
if ( empty ( $parameters [ 'database' ])) {
$parameters [ 'database' ] = 'database' . self :: database_extension ();
2010-01-17 11:48:22 +01:00
}
2013-04-03 06:49:59 +02:00
$dbName = $parameters [ 'database' ];
if ( ! self :: is_valid_database_name ( $dbName )) {
// If not using the correct file extension for database files then the
// results of SQLite3SchemaManager::databaseList will be unpredictable
$extension = self :: database_extension ();
Deprecation :: notice ( '3.2' , " SQLite3Database now expects a database file with extension \" $extension\ " . Behaviour may be unpredictable otherwise . " );
2010-01-24 07:57:42 +01:00
}
2010-01-17 11:48:22 +01:00
2013-04-03 06:49:59 +02:00
// use the very lightspeed SQLite In-Memory feature for testing
if ( $this -> getLivesInMemory ()) {
$file = ':memory:' ;
2010-01-24 07:57:42 +01:00
} else {
2013-04-03 06:49:59 +02:00
// Ensure path is given
if ( empty ( $parameters [ 'path' ])) {
$parameters [ 'path' ] = ASSETS_PATH . '/.sqlitedb' ;
2010-01-17 11:48:22 +01:00
}
2013-04-03 06:49:59 +02:00
//assumes that the path to dbname will always be provided:
$file = $parameters [ 'path' ] . '/' . $dbName ;
if ( ! file_exists ( $parameters [ 'path' ])) {
SQLiteDatabaseConfigurationHelper :: create_db_dir ( $parameters [ 'path' ]);
SQLiteDatabaseConfigurationHelper :: secure_db_dir ( $parameters [ 'path' ]);
2010-01-17 11:48:22 +01:00
}
}
2010-01-22 12:48:57 +01:00
2013-04-03 06:49:59 +02:00
// 'path' and 'database' are merged into the full file path, which
// is the format that connectors such as PDOConnector expect
$parameters [ 'filepath' ] = $file ;
2010-01-14 21:42:43 +01:00
2013-04-03 06:49:59 +02:00
// Ensure that driver is available (required by PDO)
if ( empty ( $parameters [ 'driver' ])) {
$parameters [ 'driver' ] = $this -> getDatabaseServer ();
2012-12-11 14:40:49 +01:00
}
2010-01-14 21:42:43 +01:00
2013-04-03 06:49:59 +02:00
$this -> connector -> connect ( $parameters , true );
2010-01-14 21:42:43 +01:00
2013-04-03 06:49:59 +02:00
foreach ( self :: $default_pragma as $pragma => $value ) {
$this -> setPragma ( $pragma , $value );
2010-01-17 11:48:22 +01:00
}
2010-01-14 21:42:43 +01:00
2013-04-03 06:49:59 +02:00
if ( empty ( self :: $default_pragma [ 'locking_mode' ])) {
self :: $default_pragma [ 'locking_mode' ] = $this -> getPragma ( 'locking_mode' );
2010-02-03 06:02:43 +01:00
}
}
2010-01-17 11:48:22 +01:00
/**
2013-04-03 06:49:59 +02:00
* Retrieve parameters used to connect to this SQLLite database
2010-01-17 11:48:22 +01:00
*
2013-04-03 06:49:59 +02:00
* @ return array
2010-01-21 12:32:21 +01:00
*/
2013-04-03 06:49:59 +02:00
public function getParameters () {
return $this -> parameters ;
2010-01-21 12:32:21 +01:00
}
2013-04-03 06:49:59 +02:00
public function getLivesInMemory () {
return isset ( $this -> parameters [ 'path' ]) && $this -> parameters [ 'path' ] === ':memory:' ;
2010-01-17 11:48:22 +01:00
}
2013-04-03 06:49:59 +02:00
public function supportsCollations () {
return true ;
2010-01-17 11:48:22 +01:00
}
2013-04-03 06:49:59 +02:00
public function supportsTimezoneOverride () {
return false ;
2010-01-17 11:48:22 +01:00
}
/**
2013-04-03 06:49:59 +02:00
* Execute PRAGMA commands .
2010-01-17 11:48:22 +01:00
*
2013-04-03 06:49:59 +02:00
* @ param string pragma name
* @ param string value to set
2010-01-17 11:48:22 +01:00
*/
2013-04-03 06:49:59 +02:00
public function setPragma ( $pragma , $value ) {
$this -> query ( " PRAGMA $pragma = $value " );
2010-01-17 11:48:22 +01:00
}
/**
2013-04-03 06:49:59 +02:00
* Gets pragma value .
2010-01-17 11:48:22 +01:00
*
2013-04-03 06:49:59 +02:00
* @ param string pragma name
* @ return string the pragma value
2010-01-17 11:48:22 +01:00
*/
2013-04-03 06:49:59 +02:00
public function getPragma ( $pragma ) {
return $this -> query ( " PRAGMA $pragma " ) -> value ();
2010-01-17 11:48:22 +01:00
}
2013-04-03 06:49:59 +02:00
public function getDatabaseServer () {
return " sqlite " ;
2010-01-17 11:48:22 +01:00
}
2013-04-03 06:49:59 +02:00
public function selectDatabase ( $name , $create = false , $errorLevel = E_USER_ERROR ) {
if ( ! $this -> schemaManager -> databaseExists ( $name )) {
// 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 );
2010-01-14 21:42:43 +01:00
}
2010-01-17 11:48:22 +01:00
2013-04-03 06:49:59 +02:00
// Reconnect using the existing parameters
$parameters = $this -> parameters ;
$parameters [ 'database' ] = $name ;
$this -> connect ( $parameters );
return true ;
2010-01-17 11:48:22 +01:00
}
function now (){
return " datetime('now', 'localtime') " ;
}
function random (){
return 'random()' ;
}
/**
* The core search engine configuration .
2013-04-03 06:49:59 +02:00
* @ todo There is a fulltext search for SQLite making use of virtual tables , the fts3 extension and the
* MATCH operator
2010-01-17 11:48:22 +01:00
* there are a few issues with fts :
* - shared cached lock doesn ' t allow to create virtual tables on versions prior to 3.6 . 17
* - there must not be more than one MATCH operator per statement
* - the fts3 extension needs to be available
* for now we use the MySQL implementation with the MATCH () AGAINST () uglily replaced with LIKE
*
* @ param string $keywords Keywords as a space separated string
* @ return object DataObjectSet of result pages
*/
2013-04-03 06:49:59 +02:00
public function searchEngine ( $classesToSearch , $keywords , $start , $pageLength , $sortBy = " Relevance DESC " ,
$extraFilter = " " , $booleanSearch = false , $alternativeFileFilter = " " , $invertedMatch = false
) {
$keywords = $this -> escapeString ( str_replace ( array ( '*' , '+' , '-' , '"' , '\'' ), '' , $keywords ));
2010-01-17 11:48:22 +01:00
$htmlEntityKeywords = htmlentities ( utf8_decode ( $keywords ));
$extraFilters = array ( 'SiteTree' => '' , 'File' => '' );
if ( $extraFilter ) {
$extraFilters [ 'SiteTree' ] = " AND $extraFilter " ;
if ( $alternativeFileFilter ) $extraFilters [ 'File' ] = " AND $alternativeFileFilter " ;
else $extraFilters [ 'File' ] = $extraFilters [ 'SiteTree' ];
}
// Always ensure that only pages with ShowInSearch = 1 can be searched
2011-09-15 16:03:03 +02:00
$extraFilters [ 'SiteTree' ] .= ' AND ShowInSearch <> 0' ;
// File.ShowInSearch was added later, keep the database driver backwards compatible
// by checking for its existence first
2013-04-03 06:49:59 +02:00
$fields = $this -> getSchemaManager () -> fieldList ( 'File' );
2011-09-15 16:03:03 +02:00
if ( array_key_exists ( 'ShowInSearch' , $fields )) {
$extraFilters [ 'File' ] .= " AND ShowInSearch <> 0 " ;
}
2010-01-17 11:48:22 +01:00
$limit = $start . " , " . ( int ) $pageLength ;
$notMatch = $invertedMatch ? " NOT " : " " ;
if ( $keywords ) {
$match [ 'SiteTree' ] = "
2012-12-10 23:38:35 +01:00
( Title LIKE '%$keywords%' OR MenuTitle LIKE '%$keywords%' OR Content LIKE '%$keywords%' OR MetaDescription LIKE '%$keywords%' OR
Title LIKE '%$htmlEntityKeywords%' OR MenuTitle LIKE '%$htmlEntityKeywords%' OR Content LIKE '%$htmlEntityKeywords%' OR MetaDescription LIKE '%$htmlEntityKeywords%' )
2010-01-17 11:48:22 +01:00
" ;
$match [ 'File' ] = " (Filename LIKE '% $keywords %' OR Title LIKE '% $keywords %' OR Content LIKE '% $keywords %') AND ClassName = 'File' " ;
// We make the relevance search by converting a boolean mode search into a normal one
$relevanceKeywords = $keywords ;
$htmlEntityRelevanceKeywords = $htmlEntityKeywords ;
2012-12-10 23:38:35 +01:00
$relevance [ 'SiteTree' ] = " (Title LIKE '% $relevanceKeywords %' OR MenuTitle LIKE '% $relevanceKeywords %' OR Content LIKE '% $relevanceKeywords %' OR MetaDescription LIKE '% $relevanceKeywords %') + (Title LIKE '% $htmlEntityRelevanceKeywords %' OR MenuTitle LIKE '% $htmlEntityRelevanceKeywords %' OR Content LIKE '% $htmlEntityRelevanceKeywords %' OR MetaDescription LIKE '% $htmlEntityRelevanceKeywords %') " ;
2010-01-17 11:48:22 +01:00
$relevance [ 'File' ] = " (Filename LIKE '% $relevanceKeywords %' OR Title LIKE '% $relevanceKeywords %' OR Content LIKE '% $relevanceKeywords %') " ;
} else {
$relevance [ 'SiteTree' ] = $relevance [ 'File' ] = 1 ;
$match [ 'SiteTree' ] = $match [ 'File' ] = " 1 = 1 " ;
}
// Generate initial queries and base table names
$baseClasses = array ( 'SiteTree' => '' , 'File' => '' );
2011-12-17 00:05:42 +01:00
$queries = array ();
2010-01-17 11:48:22 +01:00
foreach ( $classesToSearch as $class ) {
2012-05-08 05:19:28 +02:00
$queries [ $class ] = DataList :: create ( $class ) -> where ( $notMatch . $match [ $class ] . $extraFilters [ $class ], " " ) -> dataQuery () -> query ();
$fromArr = $queries [ $class ] -> getFrom ();
$baseClasses [ $class ] = reset ( $fromArr );
2010-01-17 11:48:22 +01:00
}
// Make column selection lists
$select = array (
2012-05-08 05:19:28 +02:00
'SiteTree' => array (
" \" ClassName \" " ,
" \" ID \" " ,
" \" ParentID \" " ,
" \" Title \" " ,
" \" URLSegment \" " ,
" \" Content \" " ,
" \" LastEdited \" " ,
" \" Created \" " ,
" NULL AS \" Filename \" " ,
" NULL AS \" Name \" " ,
" \" CanViewType \" " ,
" $relevance[SiteTree] AS Relevance "
),
'File' => array (
" \" ClassName \" " ,
" \" ID \" " ,
" NULL AS \" ParentID \" " ,
" \" Title \" " ,
" NULL AS \" URLSegment \" " ,
" \" Content \" " ,
" \" LastEdited \" " ,
" \" Created \" " ,
" \" Filename \" " ,
" \" Name \" " ,
" NULL AS \" CanViewType \" " ,
" $relevance[File] AS Relevance "
)
2010-01-17 11:48:22 +01:00
);
// Process queries
foreach ( $classesToSearch as $class ) {
// There's no need to do all that joining
2012-05-08 05:19:28 +02:00
$queries [ $class ] -> setFrom ( $baseClasses [ $class ]);
$queries [ $class ] -> setSelect ( array ());
foreach ( $select [ $class ] as $clause ) {
if ( preg_match ( '/^(.*) +AS +"?([^"]*)"?/i' , $clause , $matches )) {
$queries [ $class ] -> selectField ( $matches [ 1 ], $matches [ 2 ]);
} else {
$queries [ $class ] -> selectField ( str_replace ( '"' , '' , $clause ));
}
}
$queries [ $class ] -> setOrderBy ( array ());
2010-01-17 11:48:22 +01:00
}
// Combine queries
$querySQLs = array ();
2013-04-03 06:49:59 +02:00
$queryParameters = array ();
2010-01-17 11:48:22 +01:00
$totalCount = 0 ;
foreach ( $queries as $query ) {
2013-04-03 06:49:59 +02:00
$querySQLs [] = $query -> sql ( $parameters );
$queryParameters = array_merge ( $queryParameters , $parameters );
2010-01-17 11:48:22 +01:00
$totalCount += $query -> unlimitedRowCount ();
}
2012-05-08 05:19:28 +02:00
2010-01-17 11:48:22 +01:00
$fullQuery = implode ( " UNION " , $querySQLs ) . " ORDER BY $sortBy LIMIT $limit " ;
// Get records
2013-04-03 06:49:59 +02:00
$records = $this -> preparedQuery ( $fullQuery , $queryParameters );
2010-01-17 11:48:22 +01:00
2012-05-08 05:19:28 +02:00
foreach ( $records as $record ) {
2010-01-17 11:48:22 +01:00
$objects [] = new $record [ 'ClassName' ]( $record );
2011-10-07 11:30:05 +02:00
}
2012-05-08 05:19:28 +02:00
if ( isset ( $objects )) $doSet = new ArrayList ( $objects );
else $doSet = new ArrayList ();
$list = new PaginatedList ( $doSet );
$list -> setPageStart ( $start );
$list -> setPageLEngth ( $pageLength );
$list -> setTotalItems ( $totalCount );
return $list ;
2010-01-17 11:48:22 +01:00
}
/*
* Does this database support transactions ?
*/
public function supportsTransactions (){
2011-04-06 12:39:57 +02:00
return version_compare ( $this -> getVersion (), '3.6' , '>=' );
2010-01-17 11:48:22 +01:00
}
2013-04-03 06:49:59 +02:00
public function supportsExtensions ( $extensions = array ( 'partitions' , 'tablespaces' , 'clustering' )){
2010-01-24 07:57:42 +01:00
2010-01-17 11:48:22 +01:00
if ( isset ( $extensions [ 'partitions' ]))
return true ;
elseif ( isset ( $extensions [ 'tablespaces' ]))
return true ;
elseif ( isset ( $extensions [ 'clustering' ]))
return true ;
else
return false ;
}
2011-09-15 16:06:00 +02:00
2013-04-03 06:49:59 +02:00
public function transactionStart ( $transaction_mode = false , $session_characteristics = false ) {
$this -> query ( 'BEGIN' );
2010-01-17 11:48:22 +01:00
}
2013-04-03 06:49:59 +02:00
public function transactionSavepoint ( $savepoint ) {
$this -> query ( " SAVEPOINT \" $savepoint\ " " );
2010-01-17 11:48:22 +01:00
}
2013-04-03 06:49:59 +02:00
public function transactionRollback ( $savepoint = false ){
2010-01-17 11:48:22 +01:00
if ( $savepoint ) {
2013-04-03 06:49:59 +02:00
$this -> query ( " ROLLBACK TO $savepoint ; " );
2010-01-17 11:48:22 +01:00
} else {
2013-04-03 06:49:59 +02:00
$this -> query ( 'ROLLBACK;' );
2010-01-17 11:48:22 +01:00
}
}
2013-04-03 06:49:59 +02:00
public function transactionEnd ( $chain = false ){
$this -> query ( 'COMMIT;' );
2010-01-17 11:48:22 +01:00
}
2013-04-03 06:49:59 +02:00
public function clearTable ( $table ) {
$this -> query ( " DELETE FROM \" $table\ " " );
2011-09-15 16:06:00 +02:00
}
2013-04-03 06:49:59 +02:00
public function comparisonClause ( $field , $value , $exact = false , $negate = false , $caseSensitive = null ,
$parameterised = false
) {
2012-12-11 01:43:37 +01:00
if ( $exact && ! $caseSensitive ) {
$comp = ( $negate ) ? '!=' : '=' ;
} else {
if ( $caseSensitive ) {
// GLOB uses asterisks as wildcards.
// Replace them in search string, without replacing escaped percetage signs.
$comp = 'GLOB' ;
$value = preg_replace ( '/^%([^\\\\])/' , '*$1' , $value );
$value = preg_replace ( '/([^\\\\])%$/' , '$1*' , $value );
$value = preg_replace ( '/([^\\\\])%/' , '$1*' , $value );
} else {
$comp = 'LIKE' ;
}
if ( $negate ) $comp = 'NOT ' . $comp ;
}
2013-04-03 06:49:59 +02:00
if ( $parameterised ) {
return sprintf ( " %s %s ? " , $field , $comp );
} else {
return sprintf ( " %s %s '%s' " , $field , $comp , $value );
}
2012-12-11 01:43:37 +01:00
}
2010-02-01 06:15:10 +01:00
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 );
2013-04-03 06:49:59 +02:00
2010-02-01 06:15:10 +01:00
$translate = array (
'/%i/' => '%M' ,
'/%s/' => '%S' ,
'/%U/' => '%s' ,
);
$format = preg_replace ( array_keys ( $translate ), array_values ( $translate ), $format );
$modifiers = array ();
if ( $format == '%s' && $date != 'now' ) $modifiers [] = 'utc' ;
if ( $format != '%s' && $date == 'now' ) $modifiers [] = 'localtime' ;
if ( preg_match ( '/^now$/i' , $date )) {
$date = " 'now' " ;
} else if ( preg_match ( '/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i' , $date )) {
$date = " ' $date ' " ;
}
$modifier = empty ( $modifiers ) ? '' : " , ' " . implode ( " ', ' " , $modifiers ) . " ' " ;
return " strftime(' $format ', $date $modifier ) " ;
}
2013-04-03 06:49:59 +02:00
2010-02-01 06:15:10 +01:00
function datetimeIntervalClause ( $date , $interval ) {
$modifiers = array ();
if ( $date == 'now' ) $modifiers [] = 'localtime' ;
if ( preg_match ( '/^now$/i' , $date )) {
$date = " 'now' " ;
} else if ( preg_match ( '/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i' , $date )) {
$date = " ' $date ' " ;
}
$modifier = empty ( $modifiers ) ? '' : " , ' " . implode ( " ', ' " , $modifiers ) . " ' " ;
return " datetime( $date $modifier , ' $interval ') " ;
}
function datetimeDifferenceClause ( $date1 , $date2 ) {
$modifiers1 = array ();
$modifiers2 = array ();
if ( $date1 == 'now' ) $modifiers1 [] = 'localtime' ;
if ( $date2 == 'now' ) $modifiers2 [] = 'localtime' ;
if ( preg_match ( '/^now$/i' , $date1 )) {
$date1 = " 'now' " ;
} 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 = " 'now' " ;
} else if ( preg_match ( '/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i' , $date2 )) {
$date2 = " ' $date2 ' " ;
}
$modifier1 = empty ( $modifiers1 ) ? '' : " , ' " . implode ( " ', ' " , $modifiers1 ) . " ' " ;
$modifier2 = empty ( $modifiers2 ) ? '' : " , ' " . implode ( " ', ' " , $modifiers2 ) . " ' " ;
return " strftime('%s', $date1 $modifier1 ) - strftime('%s', $date2 $modifier2 ) " ;
}
2010-01-14 21:42:43 +01:00
}