Compare commits

..

1 Commits
2 ... 1.0.0

Author SHA1 Message Date
Andreas Piening
fc16910dad created stable tag 1.0 2010-09-20 03:45:10 +00:00
23 changed files with 1700 additions and 2402 deletions

View File

@ -1,17 +0,0 @@
# For more information about the properties used in this file,
# please see the EditorConfig documentation:
# http://editorconfig.org
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[{*.yml,package.json}]
indent_size = 2
# The indent size used in the package.json file cannot be changed:
# https://github.com/npm/npm/pull/3180#issuecomment-16336516

1
.gitattributes vendored
View File

@ -1 +0,0 @@
/.travis.yml export-ignore

View File

@ -1,11 +0,0 @@
name: CI
on:
push:
pull_request:
workflow_dispatch:
jobs:
ci:
name: CI
uses: silverstripe/gha-ci/.github/workflows/ci.yml@v1

View File

@ -1,16 +0,0 @@
name: Dispatch CI
on:
# At 3:00 PM UTC, only on Sunday and Monday
schedule:
- cron: '0 15 * * 0,1'
jobs:
dispatch-ci:
name: Dispatch CI
# Only run cron on the silverstripe account
if: (github.event_name == 'schedule' && github.repository_owner == 'silverstripe') || (github.event_name != 'schedule')
runs-on: ubuntu-latest
steps:
- name: Dispatch CI
uses: silverstripe/gha-dispatch-ci@v1

View File

@ -1,17 +0,0 @@
name: Keepalive
on:
workflow_dispatch:
# The 15th of every month at 3:50pm UTC
schedule:
- cron: '50 15 15 * *'
jobs:
keepalive:
name: Keepalive
# Only run cron on the silverstripe account
if: (github.event_name == 'schedule' && github.repository_owner == 'silverstripe') || (github.event_name != 'schedule')
runs-on: ubuntu-latest
steps:
- name: Keepalive
uses: silverstripe/gha-keepalive@v1

View File

@ -1,7 +0,0 @@
mappings:
SQLite3Connector: SilverStripe\SQLite\SQLite3Connector
SQLite3Database: SilverStripe\SQLite\SQLite3Database
SQLite3Query: SilverStripe\SQLite\SQLite3Query
SQLite3QueryBuilder: SilverStripe\SQLite\SQLite3QueryBuilder
SQLite3SchemaManager: SilverStripe\SQLite\SQLite3SchemaManager
SQLiteDatabaseConfigurationHelper: SilverStripe\SQLite\SQLiteDatabaseConfigurationHelper

17
LICENSE
View File

@ -1,17 +0,0 @@
Copyright (c) 2013, SilverStripe Limited - www.silverstripe.com
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of SilverStripe nor the names of its contributors may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
OF SUCH DAMAGE.

44
README Normal file
View File

@ -0,0 +1,44 @@
SQLite3 Module
==============
Maintainer Contact
------------------
Andreas Piening (Nickname: apiening)
<andreas (at) silverstripe (dot) com>
Requirements
------------
SilverStripe 2.4 or newer
Installation
------------
download, unzip and copy the sqlite3 folder to your project root so that it becomes a sibling of cms, sapphire and co.
either use the installer to automatically install SQLite or add this to your _config.php (right after "require_once("conf/ConfigureFromEnv.php");" if you are using _ss_environment.php)
$databaseConfig['type'] = 'SQLiteDatabase';
you are done!
make sure the webserver has sufficient privileges to write to that folder and that it is protected from external access.
URL parameter
-------------
If you're trying change a field constrain to NOT NULL on a field that contains NULLs it aborts the action because it might corrupt existing records. In order to perform the action anyway add the URL parameter 'avoidConflict' when running dev/build which temporarily adds a conflict clause to the field spec.
E.g.: http://www.my-project.com/?avoidConflict=1
Tested stacks
-------------
OSX leopard, XAMPP with PHP 5.3.0, SQLite3.6.3
OSX leopard, MAMP with PHP 5.2.6, SQLite3.3.7
Ubuntu, PHP 5.2.4, SQLite3.4.2
WinXP, XAMPP with PHP 5.3.0, SQLite3.6.16
Open Issues
-----------
- SQLite3 is supposed to work with all may not work with certain modules as they are using custom SQL statements passed to the DB class directly ;(
- there is no real fulltext search yet and the build-in search engine is not ordering by relevance, check out fts3

View File

@ -1,67 +0,0 @@
# SQLite3 Module
[![CI](https://github.com/silverstripe/silverstripe-sqlite3/actions/workflows/ci.yml/badge.svg)](https://github.com/silverstripe/silverstripe-sqlite3/actions/workflows/ci.yml)
[![Silverstripe supported module](https://img.shields.io/badge/silverstripe-supported-0071C4.svg)](https://www.silverstripe.org/software/addons/silverstripe-commercially-supported-module-list/)
## Maintainer Contact
Andreas Piening (Nickname: apiening)
<andreas (at) silverstripe (dot) com>
## Requirements
* Silverstripe 4.0 or newer
## Installation
* Install using composer with `composer require silverstripe/sqlite3 ^2`.
## Configuration
Either use the installer to automatically install SQLite or add this to your _config.php (right after
"require_once("conf/ConfigureFromEnv.php");" if you are using _ss_environment.php)
$databaseConfig['type'] = 'SQLite3Database';
$databaseConfig['path'] = "/path/to/my/database/file";
Make sure the webserver has sufficient privileges to write to that folder and that it is protected from
external access.
### Sample mysite/_config.php
```php
<?php
global $project;
$project = 'mysite';
global $database;
$database = 'SS_mysite';
require_once("conf/ConfigureFromEnv.php");
global $databaseConfig;
$databaseConfig = array(
"type" => 'SQLite3Database',
"server" => 'none',
"username" => 'none',
"password" => 'none',
"database" => $database,
"path" => "/path/to/my/database/file",
);
```
Again: make sure that the webserver has permission to read and write to the above path (/path/to/my/database/,
'file' would be the name of the sqlite db file)
## URL parameter
If you're trying to change a field constrain to NOT NULL on a field that contains NULLs dev/build fails because
it might corrupt existing records. In order to perform the action anyway add the URL parameter 'avoidConflict' when
running dev/build which temporarily adds a conflict clause to the field spec.
E.g.: http://www.my-project.com/?avoidConflict=1
## Open Issues
- SQLite3 is supposed to work with all may not work with certain modules as they are using custom SQL statements
passed to the DB class directly ;(
- there is no real fulltext search yet and the build-in search engine is not ordering by relevance, check out fts3

View File

@ -1 +1,29 @@
<?php <?php
$classes = array('SQLiteDatabase', 'SQLite3Database', 'SQLitePDODatabase');
global $databaseConfig;
if(defined('SS_DATABASE_CLASS') && in_array(SS_DATABASE_CLASS, $classes)) {
$databaseConfig['type'] = SS_DATABASE_CLASS;
}
if(in_array($databaseConfig['type'], $classes)) {
if(empty($databaseConfig['path'])) $databaseConfig['path'] = defined('SS_SQLITE_DATABASE_PATH') && SS_SQLITE_DATABASE_PATH ? SS_SQLITE_DATABASE_PATH : ASSETS_PATH . '/.sqlitedb/'; // where to put the database file
$databaseConfig['database'] = (defined('SS_DATABASE_PREFIX') ? SS_DATABASE_PREFIX : '') . $databaseConfig['database'] . (defined('SS_DATABASE_SUFFIX') ? SS_DATABASE_SUFFIX : '');
if(!isset($databaseConfig['memory'])) $databaseConfig['memory'] = true; // run tests in memory
if(empty($databaseConfig['key'])) $databaseConfig['key'] = defined('SS_SQLITE_DATABASE_KEY') && SS_SQLITE_DATABASE_KEY ? SS_SQLITE_DATABASE_KEY : 'SQLite3DatabaseKey';
/**
* set pragma values on the connection.
* @see http://www.sqlite.org/pragma.html
*/
SQLite3Database::$default_pragma['encoding'] = '"UTF-8"';
SQLite3Database::$default_pragma['locking_mode'] = 'NORMAL';
// The SQLite3 class is available in PHP 5.3 and newer
if(class_exists('SQLite3') && $databaseConfig['type'] != 'SQLitePDODatabase') {
$databaseConfig['type'] = 'SQLite3Database';
} else {
$databaseConfig['type'] = 'SQLitePDODatabase';
}
}

View File

@ -1,36 +0,0 @@
---
name: sqlite3connectors
---
SilverStripe\Core\Injector\Injector:
SQLite3PDODatabase:
class: SilverStripe\SQLite\SQLite3Database
properties:
connector: '%$PDOConnector'
schemaManager: '%$SQLite3SchemaManager'
queryBuilder: '%$SQLite3QueryBuilder'
SQLite3Database:
class: SilverStripe\SQLite\SQLite3Database
properties:
connector: '%$SQLite3Connector'
schemaManager: '%$SQLite3SchemaManager'
queryBuilder: '%$SQLite3QueryBuilder'
# Legacy connector names
SQLiteDatabase:
class: SilverStripe\SQLite\SQLite3Database
properties:
connector: '%$SQLite3Connector'
schemaManager: '%$SQLite3SchemaManager'
queryBuilder: '%$SQLite3QueryBuilder'
SQLitePDODatabase:
class: SilverStripe\SQLite\SQLite3Database
properties:
connector: '%$SQLite3Connector'
schemaManager: '%$SQLite3SchemaManager'
queryBuilder: '%$SQLite3QueryBuilder'
SQLite3Connector:
class: SilverStripe\SQLite\SQLite3Connector
type: prototype
SQLite3SchemaManager:
class: SilverStripe\SQLite\SQLite3SchemaManager
SQLite3QueryBuilder:
class: SilverStripe\SQLite\SQLite3QueryBuilder

View File

@ -1,25 +0,0 @@
<?php
// Called from DatabaseAdapterRegistry::autoconfigure($config)
use SilverStripe\Core\Environment;
use SilverStripe\SQLite\SQLite3Database;
if (!isset($databaseConfig)) {
global $databaseConfig;
}
// Get path
$path = Environment::getEnv(SQLite3Database::ENV_PATH);
if ($path) {
$databaseConfig['path'] = $path;
} elseif (defined(SQLite3Database::ENV_PATH)) {
$databaseConfig['path'] = constant(SQLite3Database::ENV_PATH);
}
// Get key
$key = Environment::getEnv(SQLite3Database::ENV_KEY);
if ($key) {
$databaseConfig['key'] = $key;
} elseif (defined(SQLite3Database::ENV_KEY)) {
$databaseConfig['key'] = constant(SQLite3Database::ENV_KEY);
}

View File

@ -1,54 +0,0 @@
<?php
use SilverStripe\Dev\Install\DatabaseAdapterRegistry;
use SilverStripe\SQLite\SQLiteDatabaseConfigurationHelper;
$sqliteDatabaseAdapterRegistryFields = array(
'path' => array(
'title' => 'Directory path<br /><small>Absolute path to directory, writeable by the webserver user.<br />'
. 'Recommended to be outside of your webroot</small>',
'default' => dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'assets' . DIRECTORY_SEPARATOR . '.sqlitedb'
),
'database' => array(
'title' => 'Database filename (extension .sqlite)',
'default' => 'database.sqlite'
)
);
// Basic SQLLite3 Database
/** @skipUpgrade */
DatabaseAdapterRegistry::register(
array(
'class' => 'SQLite3Database',
'module' => 'sqlite3',
'title' => 'SQLite 3.3+ (using SQLite3)',
'helperPath' => __DIR__.'/code/SQLiteDatabaseConfigurationHelper.php',
'helperClass' => SQLiteDatabaseConfigurationHelper::class,
'supported' => class_exists('SQLite3'),
'missingExtensionText' => 'The <a href="http://php.net/manual/en/book.sqlite3.php">SQLite3</a>
PHP Extension is not available. Please install or enable it of them and refresh this page.',
'fields' => array_merge($sqliteDatabaseAdapterRegistryFields, array('key' => array(
'title' => 'Encryption key<br><small>This function is experimental and requires configuration of an '
. 'encryption module</small>',
'default' => ''
)))
)
);
// PDO database
/** @skipUpgrade */
DatabaseAdapterRegistry::register(
array(
'class' => 'SQLite3PDODatabase',
'module' => 'sqlite3',
'title' => 'SQLite 3.3+ (using PDO)',
'helperPath' => __DIR__.'/code/SQLiteDatabaseConfigurationHelper.php',
'helperClass' => SQLiteDatabaseConfigurationHelper::class,
'supported' => (class_exists('PDO') && in_array('sqlite', PDO::getAvailableDrivers())),
'missingExtensionText' =>
'Either the <a href="http://php.net/manual/en/book.pdo.php">PDO Extension</a> or the
<a href="http://php.net/manual/en/book.sqlite3.php">SQLite3 PDO Driver</a>
are unavailable. Please install or enable these and refresh this page.',
'fields' => $sqliteDatabaseAdapterRegistryFields
)
);

View File

@ -1 +0,0 @@
When having discussions about this module in issues or pull request please adhere to the [SilverStripe Community Code of Conduct](https://docs.silverstripe.org/en/contributing/code_of_conduct).

View File

@ -1,189 +0,0 @@
<?php
namespace SilverStripe\SQLite;
use SilverStripe\ORM\Connect\DBConnector;
use SQLite3;
/**
* SQLite connector class
*/
class SQLite3Connector extends DBConnector
{
/**
* The name of the database.
*
* @var string
*/
protected $databaseName;
/**
* Connection to the DBMS.
*
* @var SQLite3
*/
protected $dbConn;
public function connect($parameters, $selectDB = false)
{
$file = $parameters['filepath'];
$this->dbConn = empty($parameters['key'])
? new SQLite3($file, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE)
: new SQLite3($file, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, $parameters['key']);
$this->dbConn->busyTimeout(60000);
$this->databaseName = $parameters['database'];
}
public function affectedRows()
{
return $this->dbConn->changes();
}
public function getGeneratedID($table)
{
return $this->dbConn->lastInsertRowID();
}
public function getLastError()
{
$message = $this->dbConn->lastErrorMsg();
return $message === 'not an error' ? null : $message;
}
public function getSelectedDatabase()
{
return $this->databaseName;
}
public function getVersion()
{
$version = SQLite3::version();
return trim($version['versionString']);
}
public function isActive()
{
return $this->databaseName && $this->dbConn;
}
/**
* Prepares the list of parameters in preparation for passing to mysqli_stmt_bind_param
*
* @param array $parameters List of parameters
* @return array List of parameters types and values
*/
public function parsePreparedParameters($parameters)
{
$values = array();
foreach ($parameters as $value) {
$phpType = gettype($value);
$sqlType = null;
// Allow overriding of parameter type using an associative array
if ($phpType === 'array') {
$phpType = $value['type'];
$value = $value['value'];
}
// Convert php variable type to one that makes mysqli_stmt_bind_param happy
// @see http://www.php.net/manual/en/mysqli-stmt.bind-param.php
switch ($phpType) {
case 'boolean':
case 'integer':
$sqlType = SQLITE3_INTEGER;
break;
case 'float': // Not actually returnable from gettype
case 'double':
$sqlType = SQLITE3_FLOAT;
break;
case 'object': // Allowed if the object or resource has a __toString method
case 'resource':
case 'string':
$sqlType = SQLITE3_TEXT;
break;
case 'NULL':
$sqlType = SQLITE3_NULL;
break;
case 'blob':
$sqlType = SQLITE3_BLOB;
break;
case 'array':
case 'unknown type':
default:
$this->databaseError("Cannot bind parameter \"$value\" as it is an unsupported type ($phpType)");
break;
}
$values[] = array(
'type' => $sqlType,
'value' => $value
);
}
return $values;
}
public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR)
{
// Type check, identify, and prepare parameters for passing to the statement bind function
$parsedParameters = $this->parsePreparedParameters($parameters);
// Prepare statement
$statement = @$this->dbConn->prepare($sql);
if ($statement) {
// Bind and run to statement
for ($i = 0; $i < count($parsedParameters); $i++) {
$value = $parsedParameters[$i]['value'];
$type = $parsedParameters[$i]['type'];
$statement->bindValue($i+1, $value, $type);
}
// Return successful result
$handle = $statement->execute();
if ($handle) {
return new SQLite3Query($this, $handle);
}
}
// Handle error
$values = $this->parameterValues($parameters);
$this->databaseError($this->getLastError(), $errorLevel, $sql, $values);
return null;
}
public function query($sql, $errorLevel = E_USER_ERROR)
{
// Return successful result
$handle = @$this->dbConn->query($sql);
if ($handle) {
return new SQLite3Query($this, $handle);
}
// Handle error
$this->databaseError($this->getLastError(), $errorLevel, $sql);
return null;
}
public function quoteString($value)
{
return "'".$this->escapeString($value)."'";
}
public function escapeString($value)
{
return $this->dbConn->escapeString($value ?? '');
}
public function selectDatabase($name)
{
if ($name !== $this->databaseName) {
$this->databaseError("SQLite3Connector can't change databases. Please create a new database connection");
}
return true;
}
public function unloadDatabase()
{
$this->dbConn->close();
$this->databaseName = null;
}
}

View File

@ -1,683 +1,1116 @@
<?php <?php
namespace SilverStripe\SQLite;
use SilverStripe\Assets\File;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Convert;
use SilverStripe\Dev\Deprecation;
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\Connect\Database;
use SilverStripe\ORM\DataList;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\PaginatedList;
use SilverStripe\ORM\Queries\SQLSelect;
/** /**
* SQLite database controller class * SQLite connector class.
* @package SQLite3
*/ */
class SQLite3Database extends Database
{
use Configurable;
class SQLite3Database extends SS_Database {
/** /**
* Global environment config for setting 'path' * Connection to the DBMS.
* @var object
*/ */
const ENV_PATH = 'SS_SQLITE_DATABASE_PATH'; protected $dbConn;
/** /**
* Global environment config for setting 'key' * True if we are connected to a database.
* @var boolean
*/ */
const ENV_KEY = 'SS_SQLITE_DATABASE_KEY'; protected $active;
/** /**
* Extension added to every database name * The name of the database.
*
* @config
* @var string * @var string
*/ */
private static $database_extension = '.sqlite'; protected $database;
/** /*
* Database schema manager object * This holds the name of the original database
* * So if you switch to another for unit tests, you
* @var SQLite3SchemaManager * can then switch back in order to drop the temp database
*/ */
protected $schemaManager = null; protected $database_original;
/* /*
* This holds the parameters that the original connection was created with, * This holds the parameters that the original connection was created with,
* so we can switch back to it if necessary (used for unit tests) * so we can switch back to it if necessary (used for unit tests)
*
* @var array
*/ */
protected $parameters; protected $parameters;
/* /*
* if we're on a In-Memory db * if we're on a In-Memory db
*
* @var boolean
*/ */
protected $livesInMemory = false; protected $lives_in_memory = false;
/** public static $default_pragma = array();
* @var bool
*/
protected $transactionNesting = 0;
/** public static $vacuum = true;
* @var array
*/
protected $transactionSavepoints = [];
/**
* List of default pragma values
*
* @todo Migrate to SS config
*
* @var array
*/
public static $default_pragma = array(
'encoding' => '"UTF-8"',
'locking_mode' => 'NORMAL'
);
/**
* Extension used to distinguish between sqllite database files and other files.
* Required to handle multiple databases.
*
* @return string
*/
public static function database_extension()
{
return static::config()->get('database_extension');
}
/**
* 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;
}
return substr_compare($name, $extension, -strlen($extension), strlen($extension)) === 0;
}
/** /**
* Connect to a SQLite3 database. * Connect to a SQLite3 database.
* @param array $parameters An map of parameters, which should include: * @param array $parameters An map of parameters, which should include:
* - database: The database to connect to, with the correct file extension (.sqlite) * - database: The database to connect to
* - path: the path to the SQLite3 database file * - path: the path to the SQLite3 database file
* - key: the encryption key (needs testing) * - key: the encryption key (needs testing)
* - memory: use the faster In-Memory database for unit tests * - memory: use the faster In-Memory database for unit tests
*/ */
public function connect($parameters) public function __construct($parameters) {
{
if (!empty($parameters['memory'])) {
Deprecation::notice(
'1.4.0',
"\$databaseConfig['memory'] is deprecated. Use \$databaseConfig['path'] = ':memory:' instead.",
Deprecation::SCOPE_GLOBAL
);
unset($parameters['memory']);
$parameters['path'] = ':memory:';
}
//We will store these connection parameters for use elsewhere (ie, unit tests) //We will store these connection parameters for use elsewhere (ie, unit tests)
$this->parameters=$parameters; $this->parameters=$parameters;
$this->schemaManager->flushCache(); $this->connectDatabase();
// Ensure database name is set $this->database_original=$this->database;
if (empty($parameters['database'])) {
$parameters['database'] = 'database';
} }
// use the very lightspeed SQLite In-Memory feature for testing
if ($this->getLivesInMemory()) { /*
$file = ':memory:'; * Uses whatever connection details are in the $parameters array to connect to a database of a given name
} else { */
// Ensure path is given function connectDatabase(){
$path = $this->getPath(); $this->enum_map = array();
$parameters=$this->parameters;
$dbName = !isset($this->database) ? $parameters['database'] : $dbName=$this->database;
//assumes that the path to dbname will always be provided: //assumes that the path to dbname will always be provided:
$file = $path . '/' . $parameters['database'] . self::database_extension(); $file = $parameters['path'] . '/' . $dbName;
if (!file_exists($path)) {
SQLiteDatabaseConfigurationHelper::create_db_dir($path); // use the very lightspeed SQLite In-Memory feature for testing
SQLiteDatabaseConfigurationHelper::secure_db_dir($path); if(SapphireTest::using_temp_db() && $parameters['memory']) {
} $file = ':memory:';
$this->lives_in_memory = true;
} else {
$this->lives_in_memory = false;
} }
// 'path' and 'database' are merged into the full file path, which if(!file_exists($parameters['path'])) {
// is the format that connectors such as PDOConnector expect SQLiteDatabaseConfigurationHelper::create_db_dir($parameters['path']);
$parameters['filepath'] = $file; SQLiteDatabaseConfigurationHelper::secure_db_dir($parameters['path']);
// Ensure that driver is available (required by PDO)
if (empty($parameters['driver'])) {
$parameters['driver'] = $this->getDatabaseServer();
} }
$this->connector->connect($parameters, true); $this->dbConn = new SQLite3($file, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, $parameters['key']);
if(method_exists('SQLite3', 'busyTimeout')) $this->dbConn->busyTimeout(60000);
foreach (self::$default_pragma as $pragma => $value) { //By virtue of getting here, the connection is active:
$this->setPragma($pragma, $value); $this->active=true;
$this->database = $dbName;
if(!$this->dbConn) {
$this->databaseError("Couldn't connect to SQLite3 database");
return false;
} }
foreach(self::$default_pragma as $pragma => $value) $this->pragma($pragma, $value);
if(empty(self::$default_pragma['locking_mode'])) { if(empty(self::$default_pragma['locking_mode'])) {
self::$default_pragma['locking_mode'] = $this->getPragma('locking_mode'); self::$default_pragma['locking_mode'] = $this->pragma('locking_mode');
}
} }
/**
* Retrieve parameters used to connect to this SQLLite database
*
* @return array
*/
public function getParameters()
{
return $this->parameters;
}
/**
* Determine if this Db is in memory
*
* @return bool
*/
public function getLivesInMemory()
{
return isset($this->parameters['path']) && $this->parameters['path'] === ':memory:';
}
/**
* Get file path. If in memory this is null
*
* @return string|null
*/
public function getPath()
{
if ($this->getLivesInMemory()) {
return null;
}
if (empty($this->parameters['path'])) {
return ASSETS_PATH . '/.sqlitedb';
}
return $this->parameters['path'];
}
public function supportsCollations()
{
return true; return true;
} }
public function supportsTimezoneOverride() /**
{ * Not implemented, needed for PDO
return false; */
public function getConnect($parameters) {
return null;
}
/**
* Returns true if this database supports collations
* TODO: get rid of this?
* @return boolean
*/
public function supportsCollations() {
return true;
}
/**
* The version of SQLite3.
* @var float
*/
protected $sqliteVersion;
/**
* Get the version of SQLite3.
* @return float
*/
public function getVersion() {
if(!$this->sqliteVersion) {
$db_version=$this->query("SELECT sqlite_version()")->value();
$this->sqliteVersion = $db_version;
}
return $this->sqliteVersion;
} }
/** /**
* Execute PRAGMA commands. * Execute PRAGMA commands.
* * works as getter and setter for connection params
* @param string $pragma name * @param String pragma name
* @param string $value to set * @param String optional value to set
* @return String the pragma value
*/ */
public function setPragma($pragma, $value) protected function pragma($pragma, $value = null) {
{ if(strlen($value)) {
$this->query("PRAGMA $pragma = $value"); $this->query("PRAGMA $pragma = $value");
} else {
$value = $this->query("PRAGMA $pragma")->value();
}
return $value;
}
/**
* Get the database server, namely SQLite3.
* @return string
*/
public function getDatabaseServer() {
return "SQLite3";
}
public function query($sql, $errorLevel = E_USER_ERROR) {
if(isset($_REQUEST['previewwrite']) && in_array(strtolower(substr($sql,0,strpos($sql,' '))), array('insert','update','delete','replace'))) {
Debug::message("Will execute: $sql");
return;
}
if(isset($_REQUEST['showqueries'])) {
$starttime = microtime(true);
}
@$handle = $this->dbConn->query($sql);
if(isset($_REQUEST['showqueries'])) {
$endtime = round(microtime(true) - $starttime,4);
Debug::message("\n$sql\n{$endtime}ms\n", false);
}
DB::$lastQuery=$handle;
if(!$handle) {
$this->databaseError("Couldn't run query: $sql | " . $this->dbConn->lastErrorMsg(), $errorLevel);
}
return new SQLite3Query($this, $handle);
}
public function getGeneratedID($table) {
return $this->dbConn->lastInsertRowID();
} }
/** /**
* Gets pragma value. * OBSOLETE: Get the ID for the next new record for the table.
* *
* @param string $pragma name * @var string $table The name od the table.
* @return string the pragma value * @return int
*/ */
public function getPragma($pragma) public function getNextID($table) {
{ user_error('getNextID is OBSOLETE (and will no longer work properly)', E_USER_WARNING);
return $this->query("PRAGMA $pragma")->value(); $result = $this->query("SELECT MAX(ID)+1 FROM \"$table\"")->value();
return $result ? $result : 1;
} }
public function getDatabaseServer() public function isActive() {
{ return $this->active ? true : false;
return "sqlite";
} }
public function selectDatabase($name, $create = false, $errorLevel = E_USER_ERROR) /*
{ * This will create a database based on whatever is in the $this->database value
if (!$this->schemaManager->databaseExists($name)) { * So you need to have called $this->selectDatabase() first, or used the __construct method
// Check DB creation permisson */
if (!$create) { public function createDatabase() {
if ($errorLevel !== false) {
user_error("Attempted to connect to non-existing database \"$name\"", $errorLevel); $this->dbConn = null;
} $fullpath = $this->parameters['path'] . '/' . $this->database;
// Unselect database if(is_writable($fullpath)) unlink($fullpath);
$this->connector->unloadDatabase();
return false; $this->connectDatabase();
}
$this->schemaManager->createDatabase($name);
} }
// Reconnect using the existing parameters /**
$parameters = $this->parameters; * Drop the database that this object is currently connected to.
$parameters['database'] = $name; * Use with caution.
$this->connect($parameters); */
public function dropDatabase() {
//First, we need to switch back to the original database so we can drop the current one
$this->dbConn = null;
$db_to_drop=$this->database;
$this->selectDatabase($this->database_original);
$this->connectDatabase();
$fullpath = $this->parameters['path'] . '/' . $db_to_drop;
if(is_writable($fullpath)) unlink($fullpath);
}
/**
* Returns the name of the currently selected database
*/
public function currentDatabase() {
return $this->database;
}
/**
* Switches to the given database.
* If the database doesn't exist, you should call createDatabase() after calling selectDatabase()
*/
public function selectDatabase($dbname) {
$this->database=$dbname;
$this->tableList = $this->fieldList = $this->indexList = null;
return true; return true;
} }
public function now()
{ /**
* Returns true if the named database exists.
*/
public function databaseExists($name) {
$SQL_name=Convert::raw2sql($name);
$result=$this->query("PRAGMA database_list");
foreach($result as $db) if($db['name'] == 'main' && preg_match('/\/' . $name . '/', $db['file'])) return true;
if(file_exists($this->parameters['path'] . '/' . $name)) return true;
return false;
}
function beginSchemaUpdate() {
$this->pragma('locking_mode', 'EXCLUSIVE');
$this->checkAndRepairTable();
// if($this->TableExists('SQLiteEnums')) $this->query("DELETE FROM SQLiteEnums");
$this->checkAndRepairTable();
parent::beginSchemaUpdate();
}
function endSchemaUpdate() {
parent::endSchemaUpdate();
$this->pragma('locking_mode', self::$default_pragma['locking_mode']);
}
public function clearTable($table) {
if($table != 'SQLiteEnums') $this->dbConn->query("DELETE FROM \"$table\"");
}
public function createTable($table, $fields = null, $indexes = null, $options = null, $advancedOptions = null) {
if(!isset($fields['ID'])) $fields['ID'] = $this->IdColumn();
$fieldSchemata = array();
if($fields) foreach($fields as $k => $v) {
$fieldSchemata[] = "\"$k\" $v";
}
$fieldSchemas = implode(",\n",$fieldSchemata);
// Switch to "CREATE TEMPORARY TABLE" for temporary tables
$temporary = empty($options['temporary']) ? "" : "TEMPORARY";
$this->query("CREATE $temporary TABLE \"$table\" (
$fieldSchemas
)");
if($indexes) {
foreach($indexes as $indexName => $indexDetails) {
$this->createIndex($table, $indexName, $indexDetails);
}
}
return $table;
}
/**
* Alter a table's schema.
* @param $table The name of the table to alter
* @param $newFields New fields, a map of field name => field schema
* @param $newIndexes New indexes, a map of index name => index type
* @param $alteredFields Updated fields, a map of field name => field schema
* @param $alteredIndexes Updated indexes, a map of index name => index type
*/
public function alterTable($tableName, $newFields = null, $newIndexes = null, $alteredFields = null, $alteredIndexes = null, $alteredOptions = null, $advancedOptions = null) {
if($newFields) foreach($newFields as $fieldName => $fieldSpec) $this->createField($tableName, $fieldName, $fieldSpec);
if($alteredFields) foreach($alteredFields as $fieldName => $fieldSpec) $this->alterField($tableName, $fieldName, $fieldSpec);
if($newIndexes) foreach($newIndexes as $indexName => $indexSpec) $this->createIndex($tableName, $indexName, $indexSpec);
if($alteredIndexes) foreach($alteredIndexes as $indexName => $indexSpec) $this->alterIndex($tableName, $indexName, $indexSpec);
}
public function renameTable($oldTableName, $newTableName) {
$this->query("ALTER TABLE \"$oldTableName\" RENAME TO \"$newTableName\"");
}
protected static $checked_and_repaired = false;
/**
* Repairs and reindexes the table. This might take a long time on a very large table.
* @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 = null) {
$ok = true;
if(!SapphireTest::using_temp_db() && !self::$checked_and_repaired) {
$class = '';
if(get_class($this)=="SQLitePDODatabase") $class = 'PDO';
if(get_class($this)=="SQLite3Database") $class = '3';
$this->alterationMessage("SQLite$class Version " . $this->query("SELECT sqlite_version()")->value(),"repaired");
$this->alterationMessage("Checking database integrity","repaired");
if($msgs = $this->query('PRAGMA integrity_check')) foreach($msgs as $msg) if($msg['integrity_check'] != 'ok') { Debug::show($msg['integrity_check']); $ok = false; }
if(self::$vacuum) {
$this->query('VACUUM', E_USER_NOTICE);
if($this instanceof SQLitePDODatabase) {
$msg = $this->dbConn->errorInfo();
$msg = isset($msg[2]) ? $msg[2] : 'no errors';
} else {
$msg = $this->dbConn->lastErrorMsg();
}
if(preg_match('/authoriz/', $msg)) {
$this->alterationMessage('VACUUM | ' . $msg, "error");
} else {
$this->alterationMessage("VACUUMing", "repaired");
}
}
self::$checked_and_repaired = true;
}
return $ok;
}
public function createField($tableName, $fieldName, $fieldSpec) {
$this->query("ALTER TABLE \"$tableName\" ADD \"$fieldName\" $fieldSpec");
}
/**
* Change the database type of the given field.
* @param string $tableName The name of the tbale the field is in.
* @param string $fieldName The name of the field to change.
* @param string $fieldSpec The new field specification
*/
public function alterField($tableName, $fieldName, $fieldSpec) {
$oldFieldList = $this->fieldList($tableName);
$fieldNameList = '"' . implode('","', array_keys($oldFieldList)) . '"';
if(!empty($_REQUEST['avoidConflict']) && Director::isDev()) $fieldSpec = preg_replace('/\snot null\s/i', ' NOT NULL ON CONFLICT REPLACE ', $fieldSpec);
if(array_key_exists($fieldName, $oldFieldList)) {
$oldCols = array();
foreach($oldFieldList as $name => $spec) {
$newColsSpec[] = "\"$name\" " . ($name == $fieldName ? $fieldSpec : $spec);
}
$queries = array(
"BEGIN TRANSACTION",
"CREATE TABLE \"{$tableName}_alterfield_{$fieldName}\"(" . implode(',', $newColsSpec) . ")",
"INSERT INTO \"{$tableName}_alterfield_{$fieldName}\" SELECT {$fieldNameList} FROM \"$tableName\"",
"DROP TABLE \"$tableName\"",
"ALTER TABLE \"{$tableName}_alterfield_{$fieldName}\" RENAME TO \"$tableName\"",
"COMMIT"
);
$indexList = $this->indexList($tableName);
foreach($queries as $query) $this->query($query.';');
foreach($indexList as $indexName => $indexSpec) $this->createIndex($tableName, $indexName, $indexSpec);
}
}
/**
* Change the database column name of the given field.
*
* @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) {
$oldFieldList = $this->fieldList($tableName);
$oldCols = array();
if(array_key_exists($oldName, $oldFieldList)) {
foreach($oldFieldList as $name => $spec) {
$oldCols[] = "\"$name\"" . (($name == $oldName) ? " AS $newName" : '');
$newCols[] = "\"". (($name == $oldName) ? $newName : $name). "\"";
$newColsSpec[] = "\"" . (($name == $oldName) ? $newName : $name) . "\" $spec";
}
$queries = array(
"BEGIN TRANSACTION",
"CREATE TABLE \"{$tableName}_renamefield_{$oldName}\" (" . implode(',', $newColsSpec) . ")",
"INSERT INTO \"{$tableName}_renamefield_{$oldName}\" SELECT " . implode(',', $oldCols) . " FROM \"$tableName\"",
"DROP TABLE \"$tableName\"",
"ALTER TABLE \"{$tableName}_renamefield_{$oldName}\" RENAME TO \"$tableName\"",
"COMMIT"
);
$indexList = $this->indexList($tableName);
foreach($queries as $query) $this->query($query.';');
foreach($indexList as $indexName => $indexSpec) {
$renamedIndexSpec = array();
foreach(explode(',', $indexSpec) as $col) $renamedIndexSpec[] = $col == $oldName ? $newName : $col;
$this->createIndex($tableName, $indexName, implode(',', $renamedIndexSpec));
}
}
}
public function fieldList($table) {
$sqlCreate = DB::query('SELECT sql FROM sqlite_master WHERE type = "table" AND name = "' . $table . '"')->record();
$fieldList = array();
if($sqlCreate && $sqlCreate['sql']) {
preg_match('/^[\s]*CREATE[\s]+TABLE[\s]+[\'"]?[a-zA-Z0-9_]+[\'"]?[\s]*\((.+)\)[\s]*$/ims', $sqlCreate['sql'], $matches);
$fields = isset($matches[1]) ? preg_split('/,(?=(?:[^\'"]*$)|(?:[^\'"]*[\'"][^\'"]*[\'"][^\'"]*)*$)/x', $matches[1]) : array();
foreach($fields as $field) {
$details = preg_split('/\s/', trim($field));
$name = array_shift($details);
$name = str_replace('"', '', trim($name));
$fieldList[$name] = implode(' ', $details);
}
}
return $fieldList;
}
/**
* Create an index on a table.
* @param string $tableName The name of the table.
* @param string $indexName The name of the index.
* @param string $indexSpec The specification of the index, see Database::requireIndex() for more details.
*/
public function createIndex($tableName, $indexName, $indexSpec) {
$spec = $this->convertIndexSpec($indexSpec, $indexName);
if(!preg_match('/".+"/', $indexName)) $indexName = "\"$indexName\"";
$this->query("CREATE INDEX IF NOT EXISTS $indexName ON \"$tableName\" ($spec)");
}
/*
* 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, $indexName = null) {
if(is_array($indexSpec)) {
$indexSpec = $indexSpec['value'];
} else if(is_numeric($indexSpec)) {
$indexSpec = $indexName;
}
if(preg_match('/\((.+)\)/', $indexSpec, $matches)) {
$indexSpec = $matches[1];
}
return preg_replace('/\s/', '', $indexSpec);
}
/**
* prefix indexname with uppercase tablename if not yet done, in order to avoid ambiguity
*/
function getDbSqlDefinition($tableName, $indexName, $indexSpec) {
return "\"$tableName.$indexName\"";
}
/**
* Alter an index on a table.
* @param string $tableName The name of the table.
* @param string $indexName The name of the index.
* @param string $indexSpec The specification of the index, see Database::requireIndex() for more details.
*/
public function alterIndex($tableName, $indexName, $indexSpec) {
$this->createIndex($tableName, $indexName, $indexSpec);
}
/**
* Return the list of indexes in a table.
* @param string $table The table name.
* @return array
*/
public function indexList($table) {
$indexList = array();
foreach(DB::query('PRAGMA index_list("' . $table . '")') as $index) {
$list = array();
foreach(DB::query('PRAGMA index_info("' . $index["name"] . '")') as $details) $list[] = $details['name'];
$indexList[$index["name"]] = implode(',', $list);
}
return $indexList;
}
/**
* Returns a list of all the tables in the database.
* Table names will all be in lowercase.
* @return array
*/
public function tableList() {
$tables = array();
foreach($this->query('SELECT name FROM sqlite_master WHERE type = "table"') as $record) {
//$table = strtolower(reset($record));
$table = reset($record);
$tables[$table] = $table;
}
//Return an empty array if there's nothing in this database
return isset($tables) ? $tables : Array();
}
function TableExists($tableName){
$result=$this->query('SELECT name FROM sqlite_master WHERE type = "table" AND name="' . $tableName . '"')->first();
if($result)
return true;
else
return false;
}
/**
* Return the number of rows affected by the previous operation.
* @return int
*/
public function affectedRows() {
return $this->dbConn->changes();
}
/**
* Return a boolean type-formatted string
*
* @params array $values Contains a tokenised list of info about this data type
* @return string
*/
public function boolean($values){
return 'BOOL NOT NULL DEFAULT ' . (isset($values['default']) ? (int)$values['default'] : 0);
}
/**
* Return a date type-formatted string
*
* @params array $values Contains a tokenised list of info about this data type
* @return string
*/
public function date($values){
return "TEXT";
}
/**
* Return a decimal type-formatted string
*
* @params array $values Contains a tokenised list of info about this data type
* @return string
*/
public function decimal($values, $asDbValue=false){
$default = isset($values['default']) && is_numeric($values['default']) ? $values['default'] : 0;
return "NUMERIC NOT NULL DEFAULT " . $default;
}
/**
* Return a enum type-formatted string
*
* enumus are not supported. as a workaround to store allowed values we creates an additional table
*
* @params array $values Contains a tokenised list of info about this data type
* @return string
*/
protected $enum_map = array();
public function enum($values){
$tablefield = $values['table'] . '.' . $values['name'];
if(empty($this->enum_map)) $this->query("CREATE TABLE IF NOT EXISTS \"SQLiteEnums\" (\"TableColumn\" TEXT PRIMARY KEY, \"EnumList\" TEXT)");
if(empty($this->enum_map[$tablefield]) || $this->enum_map[$tablefield] != implode(',', $values['enums'])) {
$this->query("REPLACE INTO SQLiteEnums (TableColumn, EnumList) VALUES (\"{$tablefield}\", \"" . implode(',', $values['enums']) . "\")");
$this->enum_map[$tablefield] = implode(',', $values['enums']);
}
return "TEXT DEFAULT '{$values['default']}'";
}
/**
* Return a set type-formatted string
* This type doesn't exist in SQLite as well
*
* @params array $values Contains a tokenised list of info about this data type
* @return string
*/
public function set($values) {
$tablefield = $values['table'] . '.' . $values['name'];
if(empty($this->enum_map)) $this->query("CREATE TABLE IF NOT EXISTS SQLiteEnums (TableColumn TEXT PRIMARY KEY, EnumList TEXT)");
if(empty($this->enum_map[$tablefield]) || $this->enum_map[$tablefield] != implode(',', $values['enums'])) {
$this->query("REPLACE INTO SQLiteEnums (TableColumn, EnumList) VALUES (\"{$tablefield}\", \"" . implode(',', $values['enums']) . "\")");
$this->enum_map[$tablefield] = implode(',', $values['enums']);
}
$default = '';
if(!empty($values['default'])) {
$default = str_replace(array('"',"'","\\","\0"), "", $values['default']);
$default = " DEFAULT '$default'";
}
return 'TEXT' . $default;
}
/**
* Return a float type-formatted string
*
* @params array $values Contains a tokenised list of info about this data type
* @return string
*/
public function float($values, $asDbValue=false){
return "REAL";
}
/**
* Return a Double type-formatted string
*
* @params array $values Contains a tokenised list of info about this data type
* @return string
*/
public function Double($values, $asDbValue=false){
return "REAL";
}
/**
* Return a int type-formatted string
*
* @params array $values Contains a tokenised list of info about this data type
* @return string
*/
public function int($values, $asDbValue=false){
return "INTEGER({$values['precision']}) " . strtoupper($values['null']) . " DEFAULT " . (int)$values['default'];
}
/**
* Return a datetime type-formatted string
* For SQLite3, we simply return the word 'TEXT', no other parameters are necessary
*
* @params array $values Contains a tokenised list of info about this data type
* @return string
*/
public function ss_datetime($values, $asDbValue=false){
return "DATETIME";
}
/**
* Return a text type-formatted string
*
* @params array $values Contains a tokenised list of info about this data type
* @return string
*/
public function text($values, $asDbValue=false){
return 'TEXT';
}
/**
* Return a time type-formatted string
*
* @params array $values Contains a tokenised list of info about this data type
* @return string
*/
public function time($values){
return "TEXT";
}
/**
* Return a varchar type-formatted string
*
* @params array $values Contains a tokenised list of info about this data type
* @return string
*/
public function varchar($values, $asDbValue=false){
return "VARCHAR({$values['precision']}) COLLATE NOCASE";
}
/*
* Return a 4 digit numeric type. MySQL has a proprietary 'Year' type.
* For SQLite3 we use TEXT
*/
public function year($values, $asDbValue=false){
return "TEXT";
}
function escape_character($escape=false){
if($escape) return "\\\""; else return "\"";
}
/**
* This returns the column which is the primary key for each table
* In SQLite3 it is INTEGER PRIMARY KEY AUTOINCREMENT
* SQLite3 does autoincrement ids even without the AUTOINCREMENT keyword, but the behaviour is signifficantly different
*
* @return string
*/
function IdColumn($asDbValue=false){
return 'INTEGER PRIMARY KEY AUTOINCREMENT';
}
/**
* Returns true if this table exists
*/
function hasTable($tableName) {
$SQL_table = Convert::raw2sql($tableName);
return (bool)($this->query("SELECT name FROM sqlite_master WHERE type = \"table\" AND name = \"$tableName\"")->value());
}
/**
* Returns the SQL command to get all the tables in this database
*/
function allTablesSQL(){
return 'SELECT name FROM sqlite_master WHERE type = "table"';
}
/**
* Return enum values for the given field
* @return array
*/
public function enumValuesForField($tableName, $fieldName) {
$classnameinfo = DB::query("SELECT EnumList FROM SQLiteEnums WHERE TableColumn = \"{$tableName}.{$fieldName}\"")->first();
$output = array();
if($classnameinfo) {
$output = explode(',', $classnameinfo['EnumList']);
}
return $output;
}
/**
* Get the actual enum fields from the constraint value:
*/
protected function EnumValuesFromConstraint($constraint){
$constraint=substr($constraint, strpos($constraint, 'ANY (ARRAY[')+11);
$constraint=substr($constraint, 0, -11);
$constraints=Array();
$segments=explode(',', $constraint);
foreach($segments as $this_segment){
$bits=preg_split('/ *:: */', $this_segment);
array_unshift($constraints, trim($bits[0], " '"));
}
return $constraints;
}
/*
* Returns the database-specific version of the now() function
*/
function now(){
return "datetime('now', 'localtime')"; return "datetime('now', 'localtime')";
} }
public function random() /*
{ * Returns the database-specific version of the random() function
*/
function random(){
return 'random()'; return 'random()';
} }
/*
* This is a lookup table for data types.
* For instance, Postgres uses 'INT', while MySQL uses 'UNSIGNED'
* So this is a DB-specific list of equivalents.
*/
function dbDataType($type){
$values=Array(
'unsigned integer'=>'INT'
);
if(isset($values[$type]))
return $values[$type];
else return '';
}
/*
* This will return text which has been escaped in a database-friendly manner
*/
function addslashes($value){
return $this->dbConn->escapeString($value);
}
/*
* This changes the index name depending on database requirements.
*/
function modifyIndex($index, $spec){
return str_replace('"', '', $index);
}
/** /**
* The core search engine configuration. * The core search engine configuration.
* @todo There is a fulltext search for SQLite making use of virtual tables, the fts3 extension and the * @todo There is a fulltext search for SQLite making use of virtual tables, the fts3 extension and the MATCH operator
* MATCH operator
* there are a few issues with fts: * there are a few issues with fts:
* - shared cached lock doesn't allow to create virtual tables on versions prior to 3.6.17 * - 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 * - there must not be more than one MATCH operator per statement
* - the fts3 extension needs to be available * - the fts3 extension needs to be available
* for now we use the MySQL implementation with the MATCH()AGAINST() uglily replaced with LIKE * for now we use the MySQL implementation with the MATCH()AGAINST() uglily replaced with LIKE
* *
* @param array $classesToSearch
* @param string $keywords Keywords as a space separated string * @param string $keywords Keywords as a space separated string
* @param int $start * @return object DataObjectSet of result pages
* @param int $pageLength
* @param string $sortBy
* @param string $extraFilter
* @param bool $booleanSearch
* @param string $alternativeFileFilter
* @param bool $invertedMatch
* @return PaginatedList DataObjectSet of result pages
*/ */
public function searchEngine( public function searchEngine($classesToSearch, $keywords, $start, $pageLength, $sortBy = "Relevance DESC", $extraFilter = "", $booleanSearch = false, $alternativeFileFilter = "", $invertedMatch = false) {
$classesToSearch, $fileFilter = '';
$keywords, $keywords = Convert::raw2sql(str_replace(array('*','+','-','"','\''),'',$keywords));
$start,
$pageLength,
$sortBy = "Relevance DESC",
$extraFilter = "",
$booleanSearch = false,
$alternativeFileFilter = "",
$invertedMatch = false
) {
$start = (int)$start;
$pageLength = (int)$pageLength;
$keywords = $this->escapeString(str_replace(array('*', '+', '-', '"', '\''), '', $keywords));
$htmlEntityKeywords = htmlentities(utf8_decode($keywords)); $htmlEntityKeywords = htmlentities(utf8_decode($keywords));
$pageClass = 'SilverStripe\\CMS\\Model\\SiteTree'; $extraFilters = array('SiteTree' => '', 'File' => '');
$fileClass = 'SilverStripe\\Assets\\File';
$extraFilters = array($pageClass => '', $fileClass => '');
if($extraFilter) { if($extraFilter) {
$extraFilters[$pageClass] = " AND $extraFilter"; $extraFilters['SiteTree'] = " AND $extraFilter";
if ($alternativeFileFilter) { if($alternativeFileFilter) $extraFilters['File'] = " AND $alternativeFileFilter";
$extraFilters[$fileClass] = " AND $alternativeFileFilter"; else $extraFilters['File'] = $extraFilters['SiteTree'];
} else {
$extraFilters[$fileClass] = $extraFilters[$pageClass];
}
} }
// Always ensure that only pages with ShowInSearch = 1 can be searched // Always ensure that only pages with ShowInSearch = 1 can be searched
$extraFilters[$pageClass] .= ' AND ShowInSearch <> 0'; $extraFilters['SiteTree'] .= " AND ShowInSearch <> 0";
// File.ShowInSearch was added later, keep the database driver backwards compatible
// by checking for its existence first
if (File::singleton()->getSchema()->fieldSpec(File::class, 'ShowInSearch')) {
$extraFilters[$fileClass] .= " AND ShowInSearch <> 0";
}
$limit = $start . ", " . $pageLength; $limit = $start . ", " . (int) $pageLength;
$notMatch = $invertedMatch ? "NOT " : ""; $notMatch = $invertedMatch ? "NOT " : "";
if($keywords) { if($keywords) {
$match[$pageClass] = $match['SiteTree'] = "
"(Title LIKE '%$keywords%' OR MenuTitle LIKE '%$keywords%' OR Content LIKE '%$keywords%'" (Title LIKE '%$keywords%' OR MenuTitle LIKE '%$keywords%' OR Content LIKE '%$keywords%' OR MetaTitle LIKE '%$keywords%' OR MetaDescription LIKE '%$keywords%' OR MetaKeywords LIKE '%$keywords%' OR
. " OR MetaDescription LIKE '%$keywords%' OR Title LIKE '%$htmlEntityKeywords%'" Title LIKE '%$htmlEntityKeywords%' OR MenuTitle LIKE '%$htmlEntityKeywords%' OR Content LIKE '%$htmlEntityKeywords%' OR MetaTitle LIKE '%$htmlEntityKeywords%' OR MetaDescription LIKE '%$htmlEntityKeywords%' OR MetaKeywords LIKE '%$htmlEntityKeywords%')
. " OR MenuTitle LIKE '%$htmlEntityKeywords%' OR Content LIKE '%$htmlEntityKeywords%'" ";
. " OR MetaDescription LIKE '%$htmlEntityKeywords%')"; $match['File'] = "(Filename LIKE '%$keywords%' OR Title LIKE '%$keywords%' OR Content LIKE '%$keywords%') AND ClassName = 'File'";
$fileClassSQL = Convert::raw2sql($fileClass);
$match[$fileClass] =
"(Name LIKE '%$keywords%' OR Title LIKE '%$keywords%') AND ClassName = '$fileClassSQL'";
// We make the relevance search by converting a boolean mode search into a normal one // We make the relevance search by converting a boolean mode search into a normal one
$relevanceKeywords = $keywords; $relevanceKeywords = $keywords;
$htmlEntityRelevanceKeywords = $htmlEntityKeywords; $htmlEntityRelevanceKeywords = $htmlEntityKeywords;
$relevance[$pageClass] = $relevance['SiteTree'] = "(Title LIKE '%$relevanceKeywords%' OR MenuTitle LIKE '%$relevanceKeywords%' OR Content LIKE '%$relevanceKeywords%' OR MetaTitle LIKE '%$relevanceKeywords%' OR MetaDescription LIKE '%$relevanceKeywords%' OR MetaKeywords) + (Title LIKE '%$htmlEntityRelevanceKeywords%' OR MenuTitle LIKE '%$htmlEntityRelevanceKeywords%' OR Content LIKE '%$htmlEntityRelevanceKeywords%' OR MetaTitle LIKE '%$htmlEntityRelevanceKeywords%' OR MetaDescription LIKE '%$htmlEntityRelevanceKeywords%' OR MetaKeywords LIKE '%$htmlEntityRelevanceKeywords%')";
"(Title LIKE '%$relevanceKeywords%' OR MenuTitle LIKE '%$relevanceKeywords%'" $relevance['File'] = "(Filename LIKE '%$relevanceKeywords%' OR Title LIKE '%$relevanceKeywords%' OR Content 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%')";
$relevance[$fileClass] = "(Name LIKE '%$relevanceKeywords%' OR Title LIKE '%$relevanceKeywords%')";
} else { } else {
$relevance[$pageClass] = $relevance[$fileClass] = 1; $relevance['SiteTree'] = $relevance['File'] = 1;
$match[$pageClass] = $match[$fileClass] = "1 = 1"; $match['SiteTree'] = $match['File'] = "1 = 1";
} }
// Generate initial queries // Generate initial queries and base table names
$queries = array(); $baseClasses = array('SiteTree' => '', 'File' => '');
foreach($classesToSearch as $class) { foreach($classesToSearch as $class) {
$queries[$class] = DataList::create($class) $queries[$class] = singleton($class)->extendedSQL($notMatch . $match[$class] . $extraFilters[$class], "");
->where($notMatch . $match[$class] . $extraFilters[$class]) $baseClasses[$class] = reset($queries[$class]->from);
->dataQuery()
->query();
} }
// Make column selection lists // Make column selection lists
$select = array( $select = array(
$pageClass => array( 'SiteTree' => array("\"ClassName\"","\"SiteTree\".\"ID\"","\"ParentID\"", "\"Title\"","\"URLSegment\"", "\"Content\"","\"LastEdited\"","\"Created\"","NULL AS \"Filename\"", "NULL AS \"Name\"", "\"CanViewType\"", "$relevance[SiteTree] AS Relevance"),
"\"ClassName\"", 'File' => array("\"ClassName\"","\"File\".\"ID\"", "NULL AS \"ParentID\"","\"Title\"","NULL AS \"URLSegment\"","\"Content\"","\"LastEdited\"","\"Created\"","\"Filename\"", "\"Name\"", "NULL AS \"CanViewType\"", "$relevance[File] AS Relevance"),
"\"ID\"",
"\"ParentID\"",
"\"Title\"",
"\"URLSegment\"",
"\"Content\"",
"\"LastEdited\"",
"\"Created\"",
"NULL AS \"Name\"",
"\"CanViewType\"",
$relevance[$pageClass] . " AS Relevance"
),
$fileClass => array(
"\"ClassName\"",
"\"ID\"",
"NULL AS \"ParentID\"",
"\"Title\"",
"NULL AS \"URLSegment\"",
"NULL AS \"Content\"",
"\"LastEdited\"",
"\"Created\"",
"\"Name\"",
"NULL AS \"CanViewType\"",
$relevance[$fileClass] . " AS Relevance"
)
); );
// Process queries // Process queries
foreach($classesToSearch as $class) { foreach($classesToSearch as $class) {
// There's no need to do all that joining // There's no need to do all that joining
$queries[$class]->setFrom('"'.DataObject::getSchema()->baseDataTable($class).'"'); $queries[$class]->from = array(str_replace('`','',$baseClasses[$class]) => $baseClasses[$class]);
$queries[$class]->setSelect(array()); $queries[$class]->select = $select[$class];
foreach ($select[$class] as $clause) { $queries[$class]->orderby = null;
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());
} }
// Combine queries // Combine queries
$querySQLs = array(); $querySQLs = array();
$queryParameters = array();
$totalCount = 0; $totalCount = 0;
foreach($queries as $query) { foreach($queries as $query) {
/** @var SQLSelect $query */ $querySQLs[] = $query->sql();
$querySQLs[] = $query->sql($parameters);
$queryParameters = array_merge($queryParameters, $parameters);
$totalCount += $query->unlimitedRowCount(); $totalCount += $query->unlimitedRowCount();
} }
$fullQuery = implode(" UNION ", $querySQLs) . " ORDER BY $sortBy LIMIT $limit"; $fullQuery = implode(" UNION ", $querySQLs) . " ORDER BY $sortBy LIMIT $limit";
// Get records // Get records
$records = $this->preparedQuery($fullQuery, $queryParameters); $records = DB::query($fullQuery);
foreach ($records as $record) { foreach($records as $record)
$objects[] = new $record['ClassName']($record); $objects[] = new $record['ClassName']($record);
}
if (isset($objects)) { if(isset($objects)) $doSet = new DataObjectSet($objects);
$doSet = new ArrayList($objects); else $doSet = new DataObjectSet();
} else {
$doSet = new ArrayList(); $doSet->setPageLimits($start, $pageLength, $totalCount);
} return $doSet;
$list = new PaginatedList($doSet);
$list->setPageStart($start);
$list->setPageLength($pageLength);
$list->setTotalItems($totalCount);
return $list;
} }
/* /*
* Does this database support transactions? * Does this database support transactions?
*/ */
public function supportsTransactions() public function supportsTransactions(){
{ return !($this->getVersion() < 3.6);
return version_compare($this->getVersion(), '3.6', '>=');
} }
/** /*
* Does this database support transaction modes? * This is a quick lookup to discover if the database supports particular extensions
*
* SQLite doesn't support transaction modes.
*
* @param string $mode
* @return bool
*/ */
public function supportsTransactionMode(string $mode): bool public function supportsExtensions($extensions=Array('partitions', 'tablespaces', 'clustering')){
{
if(isset($extensions['partitions']))
return true;
elseif(isset($extensions['tablespaces']))
return true;
elseif(isset($extensions['clustering']))
return true;
else
return false; return false;
} }
public function supportsExtensions($extensions = array('partitions', 'tablespaces', 'clustering')) /*
{ * Start a prepared transaction
if (isset($extensions['partitions'])) {
return true;
} elseif (isset($extensions['tablespaces'])) {
return true;
} elseif (isset($extensions['clustering'])) {
return true;
} else {
return false;
}
}
public function transactionStart($transaction_mode = false, $session_characteristics = false)
{
if ($this->transactionDepth()) {
$this->transactionSavepoint('NESTEDTRANSACTION' . $this->transactionDepth());
} else {
$this->query('BEGIN');
$this->transactionDepthIncrease();
}
}
public function transactionSavepoint($savepoint)
{
$this->query("SAVEPOINT \"$savepoint\"");
$this->transactionDepthIncrease($savepoint);
}
/**
* Fetch the name of the most recent savepoint
*
* @return string
*/ */
protected function getTransactionSavepointName() public function startTransaction($transaction_mode=false, $session_characteristics=false){
{ DB::query('BEGIN');
return end($this->transactionSavepoints);
} }
public function transactionRollback($savepoint = false) /*
{ * Create a savepoint that you can jump back to if you encounter problems
// Named transaction */
public function transactionSavepoint($savepoint){
DB::query("SAVEPOINT \"$savepoint\"");
}
/*
* 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){
if($savepoint) { if($savepoint) {
$this->query("ROLLBACK TO $savepoint;"); DB::query("ROLLBACK TO $savepoint;");
$this->transactionDepthDecrease();
return true;
}
// Fail if transaction isn't available
if (!$this->transactionDepth()) {
return false;
}
if ($this->transactionIsNested()) {
$this->transactionRollback($this->getTransactionSavepointName());
} else { } else {
$this->query('ROLLBACK;'); DB::query('ROLLBACK;');
$this->transactionDepthDecrease();
} }
return true;
} }
public function transactionDepth() /*
{ * Commit everything inside this transaction so far
return $this->transactionNesting; */
} public function endTransaction(){
DB::query('COMMIT;');
public function transactionEnd($chain = false)
{
// Fail if transaction isn't available
if (!$this->transactionDepth()) {
return false;
}
if ($this->transactionIsNested()) {
$savepoint = $this->getTransactionSavepointName();
$this->query('RELEASE ' . $savepoint);
$this->transactionDepthDecrease();
} else {
$this->query('COMMIT;');
$this->resetTransactionNesting();
}
if ($chain) {
$this->transactionStart();
}
return true;
} }
/** /**
* Indicate whether or not the current transaction is nested * Convert a SQLQuery object into a SQL statement
* Returns false if there are no transactions, or the open */
* transaction is the 'outer' transaction, i.e. not nested. public function sqlQueryToString(SQLQuery $sqlQuery) {
if (!$sqlQuery->from) return '';
$distinct = $sqlQuery->distinct ? "DISTINCT " : "";
if($sqlQuery->delete) {
$text = "DELETE ";
} else if($sqlQuery->select) {
$text = "SELECT $distinct" . implode(", ", $sqlQuery->select);
}
$text .= " FROM " . implode(" ", $sqlQuery->from);
if($sqlQuery->where) $text .= " WHERE (" . $sqlQuery->getFilter(). ")";
if($sqlQuery->groupby) $text .= " GROUP BY " . implode(", ", $sqlQuery->groupby);
if($sqlQuery->having) $text .= " HAVING ( " . implode(" ) AND ( ", $sqlQuery->having) . " )";
if($sqlQuery->orderby) $text .= " ORDER BY " . $this->orderMoreSpecifically($sqlQuery->select,$sqlQuery->orderby);
if($sqlQuery->limit) {
$limit = $sqlQuery->limit;
// Pass limit as array or SQL string value
if(is_array($limit)) {
if(!array_key_exists('limit',$limit)) user_error('SQLQuery::limit(): Wrong format for $limit', E_USER_ERROR);
if(isset($limit['start']) && is_numeric($limit['start']) && isset($limit['limit']) && is_numeric($limit['limit'])) {
$combinedLimit = "$limit[limit] OFFSET $limit[start]";
} elseif(isset($limit['limit']) && is_numeric($limit['limit'])) {
$combinedLimit = (int)$limit['limit'];
} else {
$combinedLimit = false;
}
if(!empty($combinedLimit)) $text .= " LIMIT " . $combinedLimit;
} else {
$text .= " LIMIT " . $sqlQuery->limit;
}
}
return $text;
}
/**
* SQLite3 complains about ambiguous column names if the ORDER BY expression doesn't contain the table name
* and the expression matches more than one expression in the SELECT expression.
* assuming that there is no amibguity we just use the first table name
* *
* @return bool * used by SQLite3Database::sqlQueryToString()
*/
protected function transactionIsNested()
{
return $this->transactionNesting > 1;
}
/**
* Increase the nested transaction level by one
* savepoint tracking is optional because BEGIN
* opens a transaction, but is not a named reference
* *
* @param string $savepoint * @param array $select SELECT expressions as of SQLquery
* @param string $order ORDER BY expressions to be checked and augmented as of SQLquery
* @return string fully specified ORDER BY expression
*/ */
protected function transactionDepthIncrease($savepoint = null) protected function orderMoreSpecifically($select,$order) {
{
++$this->transactionNesting; $altered = false;
if ($savepoint) {
array_push($this->transactionSavepoints, $savepoint); // split expression into order terms
$terms = explode(',', $order);
foreach($terms as $i => $term) {
$term = trim($term);
// check if table is unspecified
if(!preg_match('/\./', $term)) {
$direction = '';
if(preg_match('/( ASC)$|( DESC)$/i',$term)) list($term,$direction) = explode(' ', $term);
// find a match in the SELECT array and replace
foreach($select as $s) {
if(preg_match('/"[a-z0-9_]+"\.[\'"]?' . $term . '[\'"]?/i', trim($s))) {
$terms[$i] = $s . ' ' . $direction;
$altered = true;
break;
} }
} }
}
}
return implode(',', $terms);
}
/** /**
* Decrease the nested transaction level by one * Helper functions to prepare DBMS specific SQL fragments for basic datetime operations
* and reduce the savepoint tracking if we are
* nesting, as the last one is no longer valid
*/ */
protected function transactionDepthDecrease()
{
if ($this->transactionIsNested()) {
array_pop($this->transactionSavepoints);
}
--$this->transactionNesting;
}
/** /**
* In error condition, set transactionNesting to zero * Function to return an SQL datetime expression that can be used with SQLite3
* 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
*/ */
protected function resetTransactionNesting() function formattedDatetimeClause($date, $format) {
{
$this->transactionNesting = 0;
$this->transactionSavepoints = [];
}
public function query($sql, $errorLevel = E_USER_ERROR) 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);
return parent::query($sql, $errorLevel);
}
public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR)
{
return parent::preparedQuery($sql, $parameters, $errorLevel);
}
/**
* Inspect a SQL query prior to execution
* @deprecated 2.2.0:3.0.0
* @param string $sql
*/
protected function inspectQuery($sql)
{
// no-op
}
public function clearTable($table)
{
$this->query("DELETE FROM \"$table\"");
}
public function comparisonClause(
$field,
$value,
$exact = false,
$negate = false,
$caseSensitive = null,
$parameterised = false
) {
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;
}
}
if ($parameterised) {
return sprintf("%s %s ?", $field, $comp);
} else {
return sprintf("%s %s '%s'", $field, $comp, $value);
}
}
public 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);
}
}
$translate = array( $translate = array(
'/%i/' => '%M', '/%i/' => '%M',
@ -687,16 +1120,12 @@ class SQLite3Database extends Database
$format = preg_replace(array_keys($translate), array_values($translate), $format); $format = preg_replace(array_keys($translate), array_values($translate), $format);
$modifiers = array(); $modifiers = array();
if ($format == '%s' && $date != 'now') { if($format == '%s' && $date != 'now') $modifiers[] = 'utc';
$modifiers[] = 'utc'; if($format != '%s' && $date == 'now') $modifiers[] = 'localtime';
}
if ($format != '%s' && $date == 'now') {
$modifiers[] = 'localtime';
}
if (preg_match('/^now$/i', $date ?? '')) { if(preg_match('/^now$/i', $date)) {
$date = "'now'"; $date = "'now'";
} elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date ?? '')) { } else if(preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date)) {
$date = "'$date'"; $date = "'$date'";
} }
@ -704,16 +1133,28 @@ class SQLite3Database extends Database
return "strftime('$format', $date$modifier)"; return "strftime('$format', $date$modifier)";
} }
public function datetimeIntervalClause($date, $interval) /**
{ * Function to return an SQL datetime expression that can be used with SQLite3
* 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) {
$modifiers = array(); $modifiers = array();
if ($date == 'now') { if($date == 'now') $modifiers[] = 'localtime';
$modifiers[] = 'localtime';
}
if (preg_match('/^now$/i', $date ?? '')) { if(preg_match('/^now$/i', $date)) {
$date = "'now'"; $date = "'now'";
} elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date ?? '')) { } else if(preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date)) {
$date = "'$date'"; $date = "'$date'";
} }
@ -721,27 +1162,30 @@ class SQLite3Database extends Database
return "datetime($date$modifier, '$interval')"; return "datetime($date$modifier, '$interval')";
} }
public function datetimeDifferenceClause($date1, $date2) /**
{ * Function to return an SQL datetime expression that can be used with SQLite3
* 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) {
$modifiers1 = array(); $modifiers1 = array();
$modifiers2 = array(); $modifiers2 = array();
if ($date1 == 'now') { if($date1 == 'now') $modifiers1[] = 'localtime';
$modifiers1[] = 'localtime'; if($date2 == 'now') $modifiers2[] = 'localtime';
}
if ($date2 == 'now') {
$modifiers2[] = 'localtime';
}
if (preg_match('/^now$/i', $date1 ?? '')) { if(preg_match('/^now$/i', $date1)) {
$date1 = "'now'"; $date1 = "'now'";
} elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date1 ?? '')) { } else if(preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date1)) {
$date1 = "'$date1'"; $date1 = "'$date1'";
} }
if (preg_match('/^now$/i', $date2 ?? '')) { if(preg_match('/^now$/i', $date2)) {
$date2 = "'now'"; $date2 = "'now'";
} elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date2 ?? '')) { } else if(preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date2)) {
$date2 = "'$date2'"; $date2 = "'$date2'";
} }
@ -751,3 +1195,72 @@ class SQLite3Database extends Database
return "strftime('%s', $date1$modifier1) - strftime('%s', $date2$modifier2)"; return "strftime('%s', $date1$modifier1) - strftime('%s', $date2$modifier2)";
} }
} }
/**
* A result-set from a SQLite3 database.
* @package SQLite3Database
*/
class SQLite3Query extends SS_Query {
/**
* The SQLite3Database object that created this result set.
* @var SQLite3Database
*/
protected $database;
/**
* The internal sqlite3 handle that points to the result set.
* @var resource
*/
protected $handle;
/**
* Hook the result-set given into a Query class, suitable for use by sapphire.
* @param database The database object that created this query.
* @param handle the internal sqlite3 handle that is points to the resultset.
*/
public function __construct(SQLite3Database $database, $handle) {
$this->database = $database;
$this->handle = $handle;
}
public function __destroy() {
$this->handle->finalize();
}
public function seek($row) {
$this->handle->reset();
$i=0;
while($i < $row && $row = @$this->handle->fetchArray()) $i++;
return true;
}
/**
* @todo This looks terrible but there is no SQLite3::get_num_rows() implementation
*/
public function numRecords() {
$c=0;
while(@$this->handle->fetchArray()) $c++;
$this->handle->reset();
return $c;
}
public function nextRecord() {
// Coalesce rather than replace common fields.
if($data = @$this->handle->fetchArray(SQLITE3_NUM)) {
foreach($data as $columnIdx => $value) {
if(preg_match('/^"([a-z0-9_]+)"\."([a-z0-9_]+)"$/i', $this->handle->columnName($columnIdx), $matches)) $columnName = $matches[2];
else if(preg_match('/^"([a-z0-9_]+)"$/i', $this->handle->columnName($columnIdx), $matches)) $columnName = $matches[1];
else $columnName = trim($this->handle->columnName($columnIdx),"\"' \t");
// $value || !$ouput[$columnName] means that the *last* occurring value is shown
// !$ouput[$columnName] means that the *first* occurring value is shown
if(isset($value) || !isset($output[$columnName])) {
$output[$columnName] = is_null($value) ? null : (string)$value;
}
}
return $output;
} else {
return false;
}
}
}

View File

@ -1,83 +0,0 @@
<?php
namespace SilverStripe\SQLite;
use SilverStripe\ORM\Connect\Query;
use SQLite3Result;
/**
* A result-set from a SQLite3 database.
*/
class SQLite3Query extends Query
{
/**
* The SQLite3Connector object that created this result set.
*
* @var SQLite3Connector
*/
protected $database;
/**
* The internal sqlite3 handle that points to the result set.
*
* @var SQLite3Result
*/
protected $handle;
/**
* Hook the result-set given into a Query class, suitable for use by framework.
* @param SQLite3Connector $database The database object that created this query.
* @param SQLite3Result $handle the internal sqlite3 handle that is points to the resultset.
*/
public function __construct(SQLite3Connector $database, SQLite3Result $handle)
{
$this->database = $database;
$this->handle = $handle;
}
public function __destruct()
{
if ($this->handle) {
$this->handle->finalize();
}
}
public function seek($row)
{
$this->handle->reset();
$i=0;
while ($i <= $row && $result = @$this->handle->fetchArray(SQLITE3_ASSOC)) {
$i++;
}
return $result;
}
/**
* @todo This looks terrible but there is no SQLite3::get_num_rows() implementation
*/
public function numRecords()
{
// Some queries are not iterable using fetchArray like CREATE statement
if (!$this->handle->numColumns()) {
return 0;
}
$this->handle->reset();
$c=0;
while ($this->handle->fetchArray()) {
$c++;
}
$this->handle->reset();
return $c;
}
public function nextRecord()
{
if ($data = $this->handle->fetchArray(SQLITE3_ASSOC)) {
return $data;
} else {
return false;
}
}
}

View File

@ -1,105 +0,0 @@
<?php
namespace SilverStripe\SQLite;
use SilverStripe\ORM\Queries\SQLAssignmentRow;
use SilverStripe\ORM\Queries\SQLInsert;
use SilverStripe\ORM\Queries\SQLSelect;
use SilverStripe\ORM\Connect\DBQueryBuilder;
use InvalidArgumentException;
/**
* Builds a SQL query string from a SQLExpression object
*/
class SQLite3QueryBuilder extends DBQueryBuilder
{
/**
* @param SQLInsert $query
* @param array $parameters
* @return string
*/
protected function buildInsertQuery(SQLInsert $query, array &$parameters)
{
// Multi-row insert requires SQLite specific syntax prior to 3.7.11
// For backwards compatibility reasons include the "union all select" syntax
$nl = $this->getSeparator();
$into = $query->getInto();
// Column identifiers
$columns = $query->getColumns();
// Build all rows
$rowParts = array();
foreach ($query->getRows() as $row) {
// Build all columns in this row
/** @var SQLAssignmentRow $row */
$assignments = $row->getAssignments();
// Join SET components together, considering parameters
$parts = array();
foreach ($columns as $column) {
// Check if this column has a value for this row
if (isset($assignments[$column])) {
// Assigment is a single item array, expand with a loop here
foreach ($assignments[$column] as $assignmentSQL => $assignmentParameters) {
$parts[] = $assignmentSQL;
$parameters = array_merge($parameters, $assignmentParameters);
break;
}
} else {
// This row is missing a value for a column used by another row
$parts[] = '?';
$parameters[] = null;
}
}
$rowParts[] = implode(', ', $parts);
}
$columnSQL = implode(', ', $columns);
$sql = "INSERT INTO {$into}{$nl}($columnSQL){$nl}SELECT " . implode("{$nl}UNION ALL SELECT ", $rowParts);
return $sql;
}
/**
* Return the LIMIT clause ready for inserting into a query.
*
* @param SQLSelect $query The expression object to build from
* @param array $parameters Out parameter for the resulting query parameters
* @return string The finalised limit SQL fragment
*/
public function buildLimitFragment(SQLSelect $query, array &$parameters)
{
$nl = $this->getSeparator();
// Ensure limit is given
$limit = $query->getLimit();
if (empty($limit)) {
return '';
}
// For literal values return this as the limit SQL
if (! is_array($limit)) {
return "{$nl}LIMIT $limit";
}
// Assert that the array version provides the 'limit' key
if (! array_key_exists('limit', $limit) || ($limit['limit'] !== null && ! is_numeric($limit['limit']))) {
throw new InvalidArgumentException(
'SQLite3QueryBuilder::buildLimitSQL(): Wrong format for $limit: '. var_export($limit, true)
);
}
$clause = "{$nl}";
if ($limit['limit'] !== null) {
$clause .= "LIMIT {$limit['limit']} ";
} else {
$clause .= "LIMIT -1 ";
}
if (isset($limit['start']) && is_numeric($limit['start']) && $limit['start'] !== 0) {
$clause .= "OFFSET {$limit['start']}";
}
return $clause;
}
}

View File

@ -1,741 +0,0 @@
<?php
namespace SilverStripe\SQLite;
use Exception;
use SilverStripe\Control\Director;
use SilverStripe\Dev\Debug;
use SilverStripe\ORM\Connect\DBSchemaManager;
use SQLite3;
/**
* SQLite schema manager class
*/
class SQLite3SchemaManager extends DBSchemaManager
{
/**
* Instance of the database controller this schema belongs to
*
* @var SQLite3Database
*/
protected $database = null;
/**
* Flag indicating whether or not the database has been checked and repaired
*
* @var boolean
*/
protected static $checked_and_repaired = false;
/**
* Should schema be vacuumed during checkeAndRepairTable?
*
* @var boolean
*/
public static $vacuum = true;
public function createDatabase($name)
{
// Ensure that any existing database is cleared before connection
$this->dropDatabase($name);
}
public function dropDatabase($name)
{
// No need to delete database files if operating purely within memory
if ($this->database->getLivesInMemory()) {
return;
}
// If using file based database ensure any existing file is removed
$path = $this->database->getPath();
$fullpath = $path . '/' . $name . SQLite3Database::database_extension();
if (is_writable($fullpath)) {
unlink($fullpath);
}
}
public function databaseList()
{
// If in-memory use the current database name only
if ($this->database->getLivesInMemory()) {
return array(
$this->database->getConnector()->getSelectedDatabase()
?: 'database'
);
}
// If using file based database enumerate files in the database directory
$directory = $this->database->getPath();
$files = scandir($directory);
// Filter each file in this directory
$databases = array();
if ($files !== false) {
foreach ($files as $file) {
// Filter non-files
if (!is_file("$directory/$file")) {
continue;
}
// Filter those with correct extension
if (!SQLite3Database::is_valid_database_name($file)) {
continue;
}
if ($extension = SQLite3Database::database_extension()) {
$databases[] = substr($file, 0, -strlen($extension));
} else {
$databases[] = $file;
}
}
}
return $databases;
}
public function databaseExists($name)
{
$databases = $this->databaseList();
return in_array($name, $databases);
}
/**
* Empties any cached enum values
*/
public function flushCache()
{
$this->enum_map = array();
}
public function schemaUpdate($callback)
{
// Set locking mode
$this->database->setPragma('locking_mode', 'EXCLUSIVE');
$this->checkAndRepairTable();
$this->flushCache();
// Initiate schema update
$error = null;
try {
parent::schemaUpdate($callback);
} catch (Exception $ex) {
$error = $ex;
}
// Revert locking mode
$this->database->setPragma('locking_mode', SQLite3Database::$default_pragma['locking_mode']);
if ($error) {
throw $error;
}
}
/**
* Empty a specific table
*
* @param string $table
*/
public function clearTable($table)
{
if ($table != 'SQLiteEnums') {
$this->query("DELETE FROM \"$table\"");
}
}
public function createTable($table, $fields = null, $indexes = null, $options = null, $advancedOptions = null)
{
if (!isset($fields['ID'])) {
$fields['ID'] = $this->IdColumn();
}
$fieldSchemata = array();
if ($fields) {
foreach ($fields as $k => $v) {
$fieldSchemata[] = "\"$k\" $v";
}
}
$fieldSchemas = implode(",\n", $fieldSchemata);
// Switch to "CREATE TEMPORARY TABLE" for temporary tables
$temporary = empty($options['temporary']) ? "" : "TEMPORARY";
$this->query("CREATE $temporary TABLE \"$table\" (
$fieldSchemas
)");
if ($indexes) {
foreach ($indexes as $indexName => $indexDetails) {
$this->createIndex($table, $indexName, $indexDetails);
}
}
return $table;
}
public function alterTable(
$tableName,
$newFields = null,
$newIndexes = null,
$alteredFields = null,
$alteredIndexes = null,
$alteredOptions = null,
$advancedOptions = null
) {
if ($newFields) {
foreach ($newFields as $fieldName => $fieldSpec) {
$this->createField($tableName, $fieldName, $fieldSpec);
}
}
if ($alteredFields) {
foreach ($alteredFields as $fieldName => $fieldSpec) {
$this->alterField($tableName, $fieldName, $fieldSpec);
}
}
if ($newIndexes) {
foreach ($newIndexes as $indexName => $indexSpec) {
$this->createIndex($tableName, $indexName, $indexSpec);
}
}
if ($alteredIndexes) {
foreach ($alteredIndexes as $indexName => $indexSpec) {
$this->alterIndex($tableName, $indexName, $indexSpec);
}
}
}
public function renameTable($oldTableName, $newTableName)
{
$this->query("ALTER TABLE \"$oldTableName\" RENAME TO \"$newTableName\"");
}
public function checkAndRepairTable($tableName = null)
{
$ok = true;
if (!self::$checked_and_repaired) {
$this->alterationMessage("Checking database integrity", "repaired");
// Check for any tables with failed integrity
if ($messages = $this->query('PRAGMA integrity_check')) {
foreach ($messages as $message) {
if ($message['integrity_check'] != 'ok') {
Debug::show($message['integrity_check']);
$ok = false;
}
}
}
// If enabled vacuum (clean and rebuild) the database
if (self::$vacuum) {
$this->query('VACUUM', E_USER_NOTICE);
$message = $this->database->getConnector()->getLastError();
if (preg_match('/authoriz/', $message ?? '')) {
$this->alterationMessage("VACUUM | $message", "error");
} else {
$this->alterationMessage("VACUUMing", "repaired");
}
}
self::$checked_and_repaired = true;
}
return $ok;
}
public function createField($table, $field, $spec)
{
$this->query("ALTER TABLE \"$table\" ADD \"$field\" $spec");
}
/**
* 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)
{
$oldFieldList = $this->fieldList($tableName);
$fieldNameList = '"' . implode('","', array_keys($oldFieldList)) . '"';
if (!empty($_REQUEST['avoidConflict']) && Director::isDev()) {
$fieldSpec = preg_replace('/\snot null\s/i', ' NOT NULL ON CONFLICT REPLACE ', $fieldSpec);
}
// Skip non-existing columns
if (!array_key_exists($fieldName, $oldFieldList)) {
return;
}
// Update field spec
$newColsSpec = array();
foreach ($oldFieldList as $name => $oldSpec) {
$newColsSpec[] = "\"$name\" " . ($name == $fieldName ? $fieldSpec : $oldSpec);
}
$queries = array(
"CREATE TABLE \"{$tableName}_alterfield_{$fieldName}\"(" . implode(',', $newColsSpec) . ")",
"INSERT INTO \"{$tableName}_alterfield_{$fieldName}\" SELECT {$fieldNameList} FROM \"$tableName\"",
"DROP TABLE \"$tableName\"",
"ALTER TABLE \"{$tableName}_alterfield_{$fieldName}\" RENAME TO \"$tableName\"",
);
// Remember original indexes
$indexList = $this->indexList($tableName);
// Then alter the table column
$database = $this->database;
$database->withTransaction(function () use ($database, $queries, $indexList) {
foreach ($queries as $query) {
$database->query($query . ';');
}
});
// Recreate the indexes
foreach ($indexList as $indexName => $indexSpec) {
$this->createIndex($tableName, $indexName, $indexSpec);
}
}
public function renameField($tableName, $oldName, $newName)
{
$oldFieldList = $this->fieldList($tableName);
// Skip non-existing columns
if (!array_key_exists($oldName, $oldFieldList)) {
return;
}
// Determine column mappings
$oldCols = array();
$newColsSpec = array();
foreach ($oldFieldList as $name => $spec) {
$oldCols[] = "\"$name\"" . (($name == $oldName) ? " AS $newName" : '');
$newColsSpec[] = "\"" . (($name == $oldName) ? $newName : $name) . "\" $spec";
}
// SQLite doesn't support direct renames through ALTER TABLE
$oldColsStr = implode(',', $oldCols);
$newColsSpecStr = implode(',', $newColsSpec);
$queries = array(
"CREATE TABLE \"{$tableName}_renamefield_{$oldName}\" ({$newColsSpecStr})",
"INSERT INTO \"{$tableName}_renamefield_{$oldName}\" SELECT {$oldColsStr} FROM \"$tableName\"",
"DROP TABLE \"$tableName\"",
"ALTER TABLE \"{$tableName}_renamefield_{$oldName}\" RENAME TO \"$tableName\"",
);
// Remember original indexes
$oldIndexList = $this->indexList($tableName);
// Then alter the table column
$database = $this->database;
$database->withTransaction(function () use ($database, $queries) {
foreach ($queries as $query) {
$database->query($query . ';');
}
});
// Recreate the indexes
foreach ($oldIndexList as $indexName => $indexSpec) {
// Map index columns
$columns = array_filter(array_map(function ($column) use ($newName, $oldName) {
// Unchanged
if ($column !== $oldName) {
return $column;
}
// Skip obsolete fields
if (stripos($newName, '_obsolete_') === 0) {
return null;
}
return $newName;
}, $indexSpec['columns']));
// Create index if column count unchanged
if (count($columns) === count($indexSpec['columns'])) {
$indexSpec['columns'] = $columns;
$this->createIndex($tableName, $indexName, $indexSpec);
}
}
}
public function fieldList($table)
{
$sqlCreate = $this->preparedQuery(
'SELECT "sql" FROM "sqlite_master" WHERE "type" = ? AND "name" = ?',
array('table', $table)
)->record();
$fieldList = array();
if ($sqlCreate && $sqlCreate['sql']) {
preg_match(
'/^[\s]*CREATE[\s]+TABLE[\s]+[\'"]?[a-zA-Z0-9_\\\]+[\'"]?[\s]*\((.+)\)[\s]*$/ims',
$sqlCreate['sql'] ?? '',
$matches
);
$fields = isset($matches[1])
? preg_split('/,(?=(?:[^\'"]*$)|(?:[^\'"]*[\'"][^\'"]*[\'"][^\'"]*)*$)/x', $matches[1])
: array();
foreach ($fields as $field) {
$details = preg_split('/\s/', trim($field));
$name = array_shift($details);
$name = str_replace('"', '', trim($name));
$fieldList[$name] = implode(' ', $details);
}
}
return $fieldList;
}
/**
* Create an index on a table.
*
* @param string $tableName The name of the table.
* @param string $indexName The name of the index.
* @param array $indexSpec The specification of the index, see Database::requireIndex() for more details.
*/
public function createIndex($tableName, $indexName, $indexSpec)
{
$sqliteName = $this->buildSQLiteIndexName($tableName, $indexName);
$columns = $this->implodeColumnList($indexSpec['columns']);
$unique = ($indexSpec['type'] == 'unique') ? 'UNIQUE' : '';
$this->query("CREATE $unique INDEX IF NOT EXISTS \"$sqliteName\" ON \"$tableName\" ($columns)");
}
public function alterIndex($tableName, $indexName, $indexSpec)
{
// Drop existing index
$sqliteName = $this->buildSQLiteIndexName($tableName, $indexName);
$this->query("DROP INDEX IF EXISTS \"$sqliteName\"");
// Create the index
$this->createIndex($tableName, $indexName, $indexSpec);
}
/**
* Builds the internal SQLLite index name given the silverstripe table and index name.
*
* The name is built using the table and index name in order to prevent name collisions
* between indexes of the same name across multiple tables
*
* @param string $tableName
* @param string $indexName
* @return string The SQLite3 name of the index
*/
protected function buildSQLiteIndexName($tableName, $indexName)
{
return "{$tableName}_{$indexName}";
}
public function indexKey($table, $index, $spec)
{
return $this->buildSQLiteIndexName($table, $index);
}
protected function convertIndexSpec($indexSpec)
{
$supportedIndexTypes = ['index', 'unique'];
if (isset($indexSpec['type']) && !in_array($indexSpec['type'], $supportedIndexTypes)) {
$indexSpec['type'] = 'index';
}
return parent::convertIndexSpec($indexSpec);
}
public function indexList($table)
{
$indexList = array();
// Enumerate each index and related fields
foreach ($this->query("PRAGMA index_list(\"$table\")") as $index) {
// The SQLite internal index name, not the actual Silverstripe name
$indexName = $index["name"];
$indexType = $index['unique'] ? 'unique' : 'index';
// Determine a clean list of column names within this index
$list = array();
foreach ($this->query("PRAGMA index_info(\"$indexName\")") as $details) {
$list[] = preg_replace('/^"?(.*)"?$/', '$1', $details['name']);
}
// Safely encode this spec
$indexList[$indexName] = array(
'name' => $indexName,
'columns' => $list,
'type' => $indexType,
);
}
return $indexList;
}
public function tableList()
{
$tables = array();
$result = $this->preparedQuery('SELECT name FROM sqlite_master WHERE type = ?', array('table'));
foreach ($result as $record) {
$table = reset($record);
$tables[strtolower($table)] = $table;
}
return $tables;
}
/**
* Return a boolean type-formatted string
*
* @param array $values Contains a tokenised list of info about this data type
* @return string
*/
public function boolean($values)
{
$default = empty($values['default']) ? 0 : (int)$values['default'];
return "BOOL NOT NULL DEFAULT $default";
}
/**
* Return a date type-formatted string
*
* @param array $values Contains a tokenised list of info about this data type
* @return string
*/
public function date($values)
{
return "TEXT";
}
/**
* Return a decimal type-formatted string
*
* @param array $values Contains a tokenised list of info about this data type
* @return string
*/
public function decimal($values)
{
$default = isset($values['default']) && is_numeric($values['default']) ? $values['default'] : 0;
return "NUMERIC NOT NULL DEFAULT $default";
}
/**
* Cached list of enum values indexed by table.column
*
* @var array
*/
protected $enum_map = array();
/**
* Return a enum type-formatted string
*
* enums are not supported. as a workaround to store allowed values we creates an additional table
*
* @param array $values Contains a tokenised list of info about this data type
* @return string
*/
public function enum($values)
{
$tablefield = $values['table'] . '.' . $values['name'];
$enumValues = implode(',', $values['enums']);
// Ensure the cache table exists
if (empty($this->enum_map)) {
$this->query(
"CREATE TABLE IF NOT EXISTS \"SQLiteEnums\" (\"TableColumn\" TEXT PRIMARY KEY, \"EnumList\" TEXT)"
);
}
// Ensure the table row exists
if (empty($this->enum_map[$tablefield]) || $this->enum_map[$tablefield] != $enumValues) {
$this->preparedQuery(
"REPLACE INTO SQLiteEnums (TableColumn, EnumList) VALUES (?, ?)",
array($tablefield, $enumValues)
);
$this->enum_map[$tablefield] = $enumValues;
}
// Set default
if (!empty($values['default'])) {
/*
On escaping strings:
https://www.sqlite.org/lang_expr.html
"A string constant is formed by enclosing the string in single quotes ('). A single quote within
the string can be encoded by putting two single quotes in a row - as in Pascal. C-style escapes
using the backslash character are not supported because they are not standard SQL."
Also, there is a nifty PHP function for this. However apparently one must still be cautious of
the null character ('\0' or 0x0), as per https://bugs.php.net/bug.php?id=63419
*/
$default = SQLite3::escapeString(str_replace("\0", "", $values['default']));
return "TEXT DEFAULT '$default'";
} else {
return 'TEXT';
}
}
/**
* Return a set type-formatted string
* This type doesn't exist in SQLite either
*
* @see SQLite3SchemaManager::enum()
*
* @param array $values Contains a tokenised list of info about this data type
* @return string
*/
public function set($values)
{
return $this->enum($values);
}
/**
* Return a float type-formatted string
*
* @param array $values Contains a tokenised list of info about this data type
* @return string
*/
public function float($values)
{
return "REAL";
}
/**
* Return a Double type-formatted string
*
* @param array $values Contains a tokenised list of info about this data type
* @return string
*/
public function double($values)
{
return "REAL";
}
/**
* Return a int type-formatted string
*
* @param array $values Contains a tokenised list of info about this data type
* @return string
*/
public function int($values)
{
return "INTEGER({$values['precision']}) " . strtoupper($values['null']) . " DEFAULT " . (int)$values['default'];
}
/**
* Return a bigint type-formatted string
*
* @param array $values Contains a tokenised list of info about this data type
* @return string
*/
public function bigint($values)
{
return $this->int($values);
}
/**
* Return a datetime type-formatted string
* For SQLite3, we simply return the word 'TEXT', no other parameters are necessary
*
* @param array $values Contains a tokenised list of info about this data type
* @return string
*/
public function datetime($values)
{
return "DATETIME";
}
/**
* Return a text type-formatted string
*
* @param array $values Contains a tokenised list of info about this data type
* @return string
*/
public function text($values)
{
return 'TEXT';
}
/**
* Return a time type-formatted string
*
* @param array $values Contains a tokenised list of info about this data type
* @return string
*/
public function time($values)
{
return "TEXT";
}
/**
* Return a varchar type-formatted string
*
* @param array $values Contains a tokenised list of info about this data type
* @return string
*/
public function varchar($values)
{
return "VARCHAR({$values['precision']}) COLLATE NOCASE";
}
/*
* Return a 4 digit numeric type. MySQL has a proprietary 'Year' type.
* For SQLite3 we use TEXT
*/
public function year($values, $asDbValue = false)
{
return "TEXT";
}
public function IdColumn($asDbValue = false, $hasAutoIncPK = true)
{
return 'INTEGER PRIMARY KEY AUTOINCREMENT';
}
public function hasTable($tableName)
{
return (bool)$this->preparedQuery(
'SELECT "name" FROM "sqlite_master" WHERE "type" = ? AND "name" = ?',
array('table', $tableName)
)->first();
}
/**
* Return enum values for the given field
*
* @param string $tableName
* @param string $fieldName
* @return array
*/
public function enumValuesForField($tableName, $fieldName)
{
$tablefield = "$tableName.$fieldName";
// Check already cached values for this field
if (!empty($this->enum_map[$tablefield])) {
return explode(',', $this->enum_map[$tablefield]);
}
// Retrieve and cache these details from the database
$classnameinfo = $this->preparedQuery(
"SELECT EnumList FROM SQLiteEnums WHERE TableColumn = ?",
array($tablefield)
)->first();
if ($classnameinfo) {
$valueList = $classnameinfo['EnumList'];
$this->enum_map[$tablefield] = $valueList;
return explode(',', $valueList);
}
// Fallback to empty list
return array();
}
public function dbDataType($type)
{
$values = array(
'unsigned integer' => 'INT'
);
if (isset($values[$type])) {
return $values[$type];
} else {
return '';
}
}
}

View File

@ -1,99 +1,50 @@
<?php <?php
namespace SilverStripe\SQLite;
use SilverStripe\Dev\Install\DatabaseAdapterRegistry;
use SilverStripe\Dev\Install\DatabaseConfigurationHelper;
use SQLite3;
use PDO;
use Exception;
/** /**
* This is a helper class for the SS installer. * This is a helper class for the SS installer.
* *
* It does all the specific checking for SQLiteDatabase * It does all the specific checking for SQLiteDatabase
* to ensure that the configuration is setup correctly. * to ensure that the configuration is setup correctly.
*
* @package sqlite3
*/ */
class SQLiteDatabaseConfigurationHelper implements DatabaseConfigurationHelper class SQLiteDatabaseConfigurationHelper implements DatabaseConfigurationHelper {
{
/** /**
* Create a connection of the appropriate type * Ensure that one of the database classes
* is available. If it is, we assume the PHP module for this
* database has been setup correctly.
* *
* @skipUpgrade * @param array $databaseConfig Associative array of database configuration, e.g. "type", "path" etc
* @param array $databaseConfig * @return boolean
* @param string $error Error message passed by value
* @return mixed|null Either the connection object, or null if error
*/ */
protected function createConnection($databaseConfig, &$error) public function requireDatabaseFunctions($databaseConfig) {
{ if($databaseConfig['type'] == 'SQLitePDODatabase' || version_compare(phpversion(), '5.3.0', '<')) return class_exists('PDO') ? true : false;
$error = null; return class_exists('SQLite3');
try {
if (!file_exists($databaseConfig['path'])) {
self::create_db_dir($databaseConfig['path']);
self::secure_db_dir($databaseConfig['path']);
}
$file = $databaseConfig['path'] . '/' . $databaseConfig['database'];
$conn = null;
switch ($databaseConfig['type']) {
case 'SQLite3Database':
if (empty($databaseConfig['key'])) {
$conn = @new SQLite3($file, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE);
} else {
$conn = @new SQLite3(
$file,
SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE,
$databaseConfig['key']
);
}
break;
case 'SQLite3PDODatabase':
// May throw a PDOException if fails
$conn = @new PDO("sqlite:$file");
break;
default:
$error = 'Invalid connection type: ' . $databaseConfig['type'];
return null;
} }
if ($conn) { /**
return $conn; * Ensure that the database server exists.
} else { * @param array $databaseConfig Associative array of db configuration, e.g. "type", "path" etc
$error = 'Unknown connection error'; * @return array Result - e.g. array('success' => true, 'error' => 'details of error')
return null; */
} public function requireDatabaseServer($databaseConfig) {
} catch (Exception $ex) {
$error = $ex->getMessage();
return null;
}
}
public function requireDatabaseFunctions($databaseConfig)
{
$data = DatabaseAdapterRegistry::get_adapter($databaseConfig['type']);
return !empty($data['supported']);
}
public function requireDatabaseServer($databaseConfig)
{
$path = $databaseConfig['path']; $path = $databaseConfig['path'];
$error = '';
$success = false;
if(!$path) { if(!$path) {
$success = false;
$error = 'No database path provided'; $error = 'No database path provided';
} elseif (is_writable($path) || (!file_exists($path) && is_writable(dirname($path)))) { }
// check if folder is writeable // check if parent folder is writeable
elseif(is_writable(dirname($path))) {
$success = true; $success = true;
} else { } else {
$error = "Permission denied"; $success = false;
$error = 'Webserver can\'t write database file to path "' . $path . '"';
} }
return array( return array(
'success' => $success, 'success' => $success,
'error' => $error, 'error' => $error
'path' => $path
); );
} }
@ -105,41 +56,49 @@ class SQLiteDatabaseConfigurationHelper implements DatabaseConfigurationHelper
* @param array $databaseConfig Associative array of db configuration, e.g. "type", "path" etc * @param array $databaseConfig Associative array of db configuration, e.g. "type", "path" etc
* @return array Result - e.g. array('success' => true, 'error' => 'details of error') * @return array Result - e.g. array('success' => true, 'error' => 'details of error')
*/ */
public function requireDatabaseConnection($databaseConfig) public function requireDatabaseConnection($databaseConfig) {
{ $success = false;
// Do additional validation around file paths $error = '';
if (empty($databaseConfig['path'])) {
return array(
'success' => false,
'error' => "Missing directory path"
);
}
if (empty($databaseConfig['database'])) {
return array(
'success' => false,
'error' => "Missing database filename"
);
}
// Create and secure db directory // arg validation
if(!isset($databaseConfig['path']) || !$databaseConfig['path']) return array(
'success' => false,
'error' => sprintf('Invalid path: "%s"', $databaseConfig['path'])
);
$path = $databaseConfig['path']; $path = $databaseConfig['path'];
if(!isset($databaseConfig['database']) || !$databaseConfig['database']) return array(
'success' => false,
'error' => sprintf('Invalid database name: "%s"', $databaseConfig['database'])
);
// create and secure db directory
$dirCreated = self::create_db_dir($path); $dirCreated = self::create_db_dir($path);
if (!$dirCreated) { if(!$dirCreated) return array(
return array(
'success' => false, 'success' => false,
'error' => sprintf('Cannot create path: "%s"', $path) 'error' => sprintf('Cannot create path: "%s"', $path)
); );
}
$dirSecured = self::secure_db_dir($path); $dirSecured = self::secure_db_dir($path);
if (!$dirSecured) { if(!$dirSecured) return array(
return array(
'success' => false, 'success' => false,
'error' => sprintf('Cannot secure path through .htaccess: "%s"', $path) 'error' => sprintf('Cannot secure path through .htaccess: "%s"', $path)
); );
$file = $path . '/' . $databaseConfig['database'];
$file = preg_replace('/\/$/', '', $file);
if($databaseConfig['type'] == 'SQLitePDODatabase' || version_compare(phpversion(), '5.3.0', '<')) {
$conn = @(new PDO("sqlite:$file"));
} else {
$conn = @(new SQLite3($file, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE));
} }
$conn = $this->createConnection($databaseConfig, $error); if($conn) {
$success = !empty($conn); $success = true;
} else {
$success = false;
$error = '';
}
return array( return array(
'success' => $success, 'success' => $success,
@ -148,30 +107,29 @@ class SQLiteDatabaseConfigurationHelper implements DatabaseConfigurationHelper
); );
} }
public function getDatabaseVersion($databaseConfig) public function getDatabaseVersion($databaseConfig) {
{
$version = 0; $version = 0;
/** @skipUpgrade */ if(class_exists('SQLite3')) {
switch ($databaseConfig['type']) {
case 'SQLite3Database':
$info = SQLite3::version(); $info = SQLite3::version();
if($info && isset($info['versionString'])) {
$version = trim($info['versionString']); $version = trim($info['versionString']);
break;
case 'SQLite3PDODatabase':
// Fallback to using sqlite_version() query
$conn = $this->createConnection($databaseConfig, $error);
if ($conn) {
$version = $conn->getAttribute(PDO::ATTR_SERVER_VERSION);
} }
break; } else {
// Fallback to using sqlite_version() query
$file = $databaseConfig['path'] . '/' . $databaseConfig['database'];
$file = preg_replace('/\/$/', '', $file);
$conn = @(new PDO("sqlite:$file"));
if($conn) {
$result = @$conn->query('SELECT sqlite_version()');
$version = $result->fetchColumn();
}
} }
return $version; return $version;
} }
public function requireDatabaseVersion($databaseConfig) public function requireDatabaseVersion($databaseConfig) {
{
$success = false; $success = false;
$error = ''; $error = '';
$version = $this->getDatabaseVersion($databaseConfig); $version = $this->getDatabaseVersion($databaseConfig);
@ -189,10 +147,32 @@ class SQLiteDatabaseConfigurationHelper implements DatabaseConfigurationHelper
); );
} }
public function requireDatabaseOrCreatePermissions($databaseConfig) /**
{ * Ensure that the database connection is able to use an existing database,
$conn = $this->createConnection($databaseConfig, $error); * or be able to create one if it doesn't exist.
$success = $alreadyExists = !empty($conn); *
* Unfortunately, PostgreSQLDatabase doesn't support automatically creating databases
* at the moment, so we can only check that the chosen database exists.
*
* @param array $databaseConfig Associative array of db configuration, e.g. "server", "username" etc
* @return array Result - e.g. array('success' => true, 'alreadyExists' => 'true')
*/
public function requireDatabaseOrCreatePermissions($databaseConfig) {
$success = false;
$alreadyExists = false;
$canCreate = false;
$check = $this->requireDatabaseConnection($databaseConfig);
$conn = $check['connection'];
if($conn) {
$success = true;
$alreadyExists = true;
} else {
$success = false;
$alreadyExists = false;
}
return array( return array(
'success' => $success, 'success' => $success,
'alreadyExists' => $alreadyExists, 'alreadyExists' => $alreadyExists,
@ -207,9 +187,8 @@ class SQLiteDatabaseConfigurationHelper implements DatabaseConfigurationHelper
* @param String $path Absolute path, usually with a hidden folder. * @param String $path Absolute path, usually with a hidden folder.
* @return boolean * @return boolean
*/ */
public static function create_db_dir($path) public static function create_db_dir($path) {
{ return (!file_exists($path)) ? mkdir($path) : true;
return file_exists($path) || mkdir($path);
} }
/** /**
@ -222,17 +201,7 @@ class SQLiteDatabaseConfigurationHelper implements DatabaseConfigurationHelper
* @param String $path Absolute path, containing a SQLite datatbase * @param String $path Absolute path, containing a SQLite datatbase
* @return boolean * @return boolean
*/ */
public static function secure_db_dir($path) public static function secure_db_dir($path) {
{
return (is_writeable($path)) ? file_put_contents($path . '/.htaccess', 'deny from all') : false; return (is_writeable($path)) ? file_put_contents($path . '/.htaccess', 'deny from all') : false;
} }
public function requireDatabaseAlterPermissions($databaseConfig)
{
// no concept of table-specific permissions; If you can connect you can alter schema
return array(
'success' => true,
'applies' => false
);
}
} }

177
code/SQLitePDODatabase.php Normal file
View File

@ -0,0 +1,177 @@
<?php
/**
* SQLite connector class.
* @package SQLite3
*/
class SQLitePDODatabase extends SQLite3Database {
/*
* Uses whatever connection details are in the $parameters array to connect to a database of a given name
*/
function connectDatabase(){
$this->enum_map = array();
$parameters=$this->parameters;
$dbName = !isset($this->database) ? $parameters['database'] : $dbName=$this->database;
//assumes that the path to dbname will always be provided:
$file = $parameters['path'] . '/' . $dbName;
// use the very lightspeed SQLite In-Memory feature for testing
if(SapphireTest::using_temp_db() && $parameters['memory']) {
$file = ':memory:';
$this->lives_in_memory = true;
} else {
$this->lives_in_memory = false;
}
if(!file_exists($parameters['path'])) {
SQLiteDatabaseConfigurationHelper::create_db_dir($parameters['path']);
SQLiteDatabaseConfigurationHelper::secure_db_dir($parameters['path']);
}
$this->dbConn = new PDO("sqlite:$file");
//By virtue of getting here, the connection is active:
$this->active=true;
$this->database = $dbName;
if(!$this->dbConn) {
$this->databaseError("Couldn't connect to SQLite3 database");
return false;
}
foreach(self::$default_pragma as $pragma => $value) $this->pragma($pragma, $value);
if(empty(self::$default_pragma['locking_mode'])) {
self::$default_pragma['locking_mode'] = $this->pragma('locking_mode');
}
return true;
}
public function query($sql, $errorLevel = E_USER_ERROR) {
if(isset($_REQUEST['previewwrite']) && in_array(strtolower(substr($sql,0,strpos($sql,' '))), array('insert','update','delete','replace'))) {
Debug::message("Will execute: $sql");
return;
}
if(isset($_REQUEST['showqueries'])) {
$starttime = microtime(true);
}
// @todo This is a very ugly hack to rewrite the update statement of SiteTree::doPublish()
// @see SiteTree::doPublish() There is a hack for MySQL already, maybe it's worth moving this to SiteTree or that other hack to Database...
if(preg_replace('/[\W\d]*/i','',$sql) == 'UPDATESiteTree_LiveSETSortSiteTreeSortFROMSiteTreeWHERESiteTree_LiveIDSiteTreeIDANDSiteTree_LiveParentID') {
preg_match('/\d+/i',$sql,$matches);
$sql = 'UPDATE "SiteTree_Live"
SET "Sort" = (SELECT "SiteTree"."Sort" FROM "SiteTree" WHERE "SiteTree_Live"."ID" = "SiteTree"."ID")
WHERE "ParentID" = ' . $matches[0];
}
@$handle = $this->dbConn->query($sql);
if(isset($_REQUEST['showqueries'])) {
$endtime = round(microtime(true) - $starttime,4);
Debug::message("\n$sql\n{$endtime}ms\n", false);
}
DB::$lastQuery=$handle;
if(!$handle && $errorLevel) {
$msg = $this->dbConn->errorInfo();
$this->databaseError("Couldn't run query: $sql | " . $msg[2], $errorLevel);
}
return new SQLitePDOQuery($this, $handle);
}
public function getGeneratedID($table) {
return $this->dbConn->lastInsertId();
}
/*
* This will return text which has been escaped in a database-friendly manner
*/
function addslashes($value){
return str_replace("'", "''", $value);
}
}
/**
* A result-set from a SQLitePDO database.
* @package SQLite3
*/
class SQLitePDOQuery extends SQLite3Query {
/**
* Hook the result-set given into a Query class, suitable for use by sapphire.
* @param database The database object that created this query.
* @param handle the internal sqlitePDO handle that is points to the resultset.
*/
public function __construct(SQLitePDODatabase $database, PDOStatement $handle) {
$this->database = $database;
$this->handle = $handle;
}
public function __destruct() {
if($this->handle) $this->handle->closeCursor();
}
public function __destroy() {
$this->handle->closeCursor();
}
public function seek($row) {
$this->handle->execute();
$i=0;
while($i < $row && $row = $this->handle->fetch()) $i++;
return (bool) $row;
}
public function numRecords() {
return $this->handle->rowCount();
}
public function nextRecord() {
$this->handle->setFetchMode( PDO::FETCH_CLASS, 'ResultRow');
if($data = $this->handle->fetch(PDO::FETCH_CLASS)) {
foreach($data->get() as $columnName => $value) {
if(preg_match('/^"([a-z0-9_]+)"\."([a-z0-9_]+)"$/i', $columnName, $matches)) $columnName = $matches[2];
else if(preg_match('/^"([a-z0-9_]+)"$/i', $columnName, $matches)) $columnName = $matches[1];
else $columnName = trim($columnName,"\"' \t");
$output[$columnName] = is_null($value) ? null : (string)$value;
}
return $output;
} else {
return false;
}
}
}
/**
* This is necessary for a case where we have ambigous fields in the result.
* E.g. we have something like the following:
* SELECT Child1.value, Child2.value FROM Parent LEFT JOIN Child1 LEFT JOIN Child2
* We get value twice in the result set. We want the last not empty value.
* The fetch assoc syntax does'nt work because it gives us the last value everytime, empty or not.
* The fetch num does'nt work because there is no function to retrieve the field names to create the map.
* In this approach we make use of PDO fetch class to pass the result values to an
* object and let the __set() function do the magic decision to choose the right value.
*/
class ResultRow {
private $_datamap=array();
function __set($key,$val) {
if($val || !isset($this->_datamap[$key])) $this->_datamap[$key] = $val;
}
function get() {
return $this->_datamap;
}
}

View File

@ -1,34 +0,0 @@
{
"name": "silverstripe/sqlite3",
"description": "Adds SQLite3 support to SilverStripe",
"type": "silverstripe-vendormodule",
"keywords": ["silverstripe", "sqlite3", "database"],
"authors": [
{
"name": "Ingo Schommer",
"email": "ingo@silverstripe.com"
},
{
"name": "Sean Harvey",
"email": "sean@silverstripe.com"
}
],
"require": {
"silverstripe/framework": "~4.0",
"silverstripe/vendor-plugin": "^1.0"
},
"require-dev": {
"squizlabs/php_codesniffer": "^3"
},
"autoload": {
"psr-4": {
"SilverStripe\\SQLite\\": "code/"
}
},
"scripts": {
"lint": "phpcs code/ *.php",
"lint-clean": "phpcbf code/ *.php"
},
"minimum-stability": "dev",
"prefer-stable": true
}

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ruleset name="SilverStripe">
<description>CodeSniffer ruleset for SilverStripe coding conventions.</description>
<file>code</file>
<!-- base rules are PSR-2 -->
<rule ref="PSR2" >
<!-- Current exclusions -->
<exclude name="PSR1.Methods.CamelCapsMethodName" />
</rule>
</ruleset>