diff --git a/.travis.yml b/.travis.yml index d689a48..05da9ec 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,35 +6,50 @@ cache: directories: - $HOME/.composer/cache/files -php: - - 7.1 - - 7.2 - - nightly - env: global: - DB=SQLITE - - PDO=1 matrix: fast_finish: true include: + - php: 5.6 + env: + - CORE_VERSION=1.0.x-dev + - PDO=0 + + - php: 7.0 + env: + - CORE_VERSION=1.1.x-dev + - PDO=1 + + - php: 7.1 + env: + - CORE_VERSION=4.2.x-dev + - PDO=0 + - php: 7.2 - env: PDO=0 PHPCS_TEST=1 - allow_failure: - - php: nightly + env: + - CORE_VERSION=4.3.x-dev + - PDO=0 + + - php: 7.3 + env: + - CORE_VERSION=4.x-dev + - PDO=1 + - PHPCS_TEST=1 before_script: # Init PHP - phpenv rehash - - phpenv config-rm xdebug.ini + - phpenv config-rm xdebug.ini || true - export PATH=~/.composer/vendor/bin:$PATH - echo 'memory_limit = 2048M' >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini # Install composer dependencies - composer validate - - composer require --no-update silverstripe/recipe-cms:2.x-dev - - composer install --prefer-source --no-interaction --no-progress --no-suggest --optimize-autoloader --verbose --profile + - composer require --no-update silverstripe/recipe-cms:$CORE_VERSION + - composer install --no-interaction --no-progress --no-suggest --optimize-autoloader --verbose --profile - if [[ $PHPCS_TEST ]]; then composer global require squizlabs/php_codesniffer:^3 --prefer-dist --no-interaction --no-progress --no-suggest -o; fi script: diff --git a/README.md b/README.md index c742a43..1aa76bc 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # SQLite3 Module -[![Build Status](https://travis-ci.org/silverstripe-labs/silverstripe-sqlite3.png?branch=master)](https://travis-ci.org/silverstripe-labs/silverstripe-sqlite3) +[![Build Status](https://travis-ci.org/silverstripe/silverstripe-sqlite3.png?branch=master)](https://travis-ci.org/silverstripe/silverstripe-sqlite3) +[![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 diff --git a/_config/connectors.yml b/_config/connectors.yml index 0178a62..7d79295 100644 --- a/_config/connectors.yml +++ b/_config/connectors.yml @@ -5,28 +5,28 @@ SilverStripe\Core\Injector\Injector: SQLite3PDODatabase: class: SilverStripe\SQLite\SQLite3Database properties: - connector: %$PDOConnector - schemaManager: %$SQLite3SchemaManager - queryBuilder: %$SQLite3QueryBuilder + connector: '%$PDOConnector' + schemaManager: '%$SQLite3SchemaManager' + queryBuilder: '%$SQLite3QueryBuilder' SQLite3Database: class: SilverStripe\SQLite\SQLite3Database properties: - connector: %$SQLite3Connector - schemaManager: %$SQLite3SchemaManager - queryBuilder: %$SQLite3QueryBuilder + connector: '%$SQLite3Connector' + schemaManager: '%$SQLite3SchemaManager' + queryBuilder: '%$SQLite3QueryBuilder' # Legacy connector names SQLiteDatabase: class: SilverStripe\SQLite\SQLite3Database properties: - connector: %$SQLite3Connector - schemaManager: %$SQLite3SchemaManager - queryBuilder: %$SQLite3QueryBuilder + connector: '%$SQLite3Connector' + schemaManager: '%$SQLite3SchemaManager' + queryBuilder: '%$SQLite3QueryBuilder' SQLitePDODatabase: class: SilverStripe\SQLite\SQLite3Database properties: - connector: %$SQLite3Connector - schemaManager: %$SQLite3SchemaManager - queryBuilder: %$SQLite3QueryBuilder + connector: '%$SQLite3Connector' + schemaManager: '%$SQLite3SchemaManager' + queryBuilder: '%$SQLite3QueryBuilder' SQLite3Connector: class: SilverStripe\SQLite\SQLite3Connector type: prototype diff --git a/code/SQLite3Database.php b/code/SQLite3Database.php index 02d5082..0575019 100644 --- a/code/SQLite3Database.php +++ b/code/SQLite3Database.php @@ -60,6 +60,16 @@ class SQLite3Database extends Database */ protected $livesInMemory = false; + /** + * @var bool + */ + protected $transactionNesting = 0; + + /** + * @var array + */ + protected $transactionSavepoints = []; + /** * List of default pragma values * @@ -348,7 +358,7 @@ class SQLite3Database extends Database "(Title LIKE '%$relevanceKeywords%' OR MenuTitle LIKE '%$relevanceKeywords%'" . " OR Content LIKE '%$relevanceKeywords%' OR MetaDescription LIKE '%$relevanceKeywords%')" . " + (Title LIKE '%$htmlEntityRelevanceKeywords%' OR MenuTitle LIKE '%$htmlEntityRelevanceKeywords%'" - . " OR Content LIKE '%$htmlEntityRelevanceKeywords%' OR MetaDescriptio " + . " OR Content LIKE '%$htmlEntityRelevanceKeywords%' OR MetaDescription " . " LIKE '%$htmlEntityRelevanceKeywords%')"; $relevance[$fileClass] = "(Name LIKE '%$relevanceKeywords%' OR Title LIKE '%$relevanceKeywords%')"; } else { @@ -465,26 +475,148 @@ class SQLite3Database extends Database public function transactionStart($transaction_mode = false, $session_characteristics = false) { - $this->query('BEGIN'); + 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() + { + return end($this->transactionSavepoints); } public function transactionRollback($savepoint = false) { + // Named transaction if ($savepoint) { $this->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 { $this->query('ROLLBACK;'); + $this->transactionDepthDecrease(); } + return true; + } + + public function transactionDepth() + { + return $this->transactionNesting; } public function transactionEnd($chain = false) { - $this->query('COMMIT;'); + // 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 + * Returns false if there are no transactions, or the open + * transaction is the 'outer' transaction, i.e. not nested. + * + * @return bool + */ + 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 + */ + protected function transactionDepthIncrease($savepoint = null) + { + ++$this->transactionNesting; + if ($savepoint) { + array_push($this->transactionSavepoints, $savepoint); + } + } + + /** + * Decrease the nested transaction level by one + * 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 + */ + protected function resetTransactionNesting() + { + $this->transactionNesting = 0; + $this->transactionSavepoints = []; + } + + public function query($sql, $errorLevel = E_USER_ERROR) + { + 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) diff --git a/code/SQLite3Query.php b/code/SQLite3Query.php index 82a5ac7..c16d7bc 100644 --- a/code/SQLite3Query.php +++ b/code/SQLite3Query.php @@ -55,6 +55,11 @@ class SQLite3Query extends Query */ 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()) { diff --git a/code/SQLite3SchemaManager.php b/code/SQLite3SchemaManager.php index 91c47eb..d02fa89 100644 --- a/code/SQLite3SchemaManager.php +++ b/code/SQLite3SchemaManager.php @@ -2,10 +2,11 @@ namespace SilverStripe\SQLite; +use Exception; use SilverStripe\Control\Director; use SilverStripe\Dev\Debug; use SilverStripe\ORM\Connect\DBSchemaManager; -use Exception; +use SQLite3; /** * SQLite schema manager class @@ -275,21 +276,22 @@ class SQLite3SchemaManager extends DBSchemaManager } $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" ); // Remember original indexes $indexList = $this->indexList($tableName); // Then alter the table column - foreach ($queries as $query) { - $this->query($query.';'); - } + $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) { @@ -318,21 +320,22 @@ class SQLite3SchemaManager extends DBSchemaManager $oldColsStr = implode(',', $oldCols); $newColsSpecStr = implode(',', $newColsSpec); $queries = array( - "BEGIN TRANSACTION", "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\"", - "COMMIT" ); // Remember original indexes $oldIndexList = $this->indexList($tableName); // Then alter the table column - foreach ($queries as $query) { - $this->query($query.';'); - } + $database = $this->database; + $database->withTransaction(function () use ($database, $queries) { + foreach ($queries as $query) { + $database->query($query . ';'); + } + }); // Recreate the indexes foreach ($oldIndexList as $indexName => $indexSpec) { @@ -429,6 +432,15 @@ class SQLite3SchemaManager extends DBSchemaManager 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(); @@ -540,7 +552,18 @@ class SQLite3SchemaManager extends DBSchemaManager // Set default if (!empty($values['default'])) { - $default = str_replace(array('"', "'", "\\", "\0"), "", $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';