Compare commits

...

90 Commits
1.4.0 ... 2

Author SHA1 Message Date
Steve Boyd
df532bb741
MNT Use gha-dispatch-ci (#71) 2023-03-20 16:31:41 +13:00
Will Rossiter
1a87b18e71
fix: remove deprecated function 2023-01-24 20:04:50 +13:00
Guy Sartorelli
ee7f0e4d25
Merge pull request #65 from lekoala/patch-1
prevent php 8 complaining about null values
2022-09-06 15:59:32 +12:00
Thomas Portelange
66c1c09b74
switch back to null
users are expected to use ?? '' if needed
2022-08-31 09:00:08 +02:00
Thomas Portelange
d3d03d9f79
Update code/SQLite3Connector.php
Co-authored-by: Guy Sartorelli <36352093+GuySartorelli@users.noreply.github.com>
2022-08-31 08:58:48 +02:00
Guy Sartorelli
f949becc6a
Merge branch '2.1' into 2 2022-08-31 12:58:44 +12:00
Thomas Portelange
ecaadc029e
Enforce proper type
Otherwise it may fail when passed to preg_match

Deprecated: preg_match(): Passing null to parameter #2 ($subject) of type string is deprecated
2022-08-19 15:07:23 +02:00
Thomas Portelange
05abb3f483
prevent php 8 complaining about null values
Fix Deprecated: SQLite3::escapeString(): Passing null to parameter #1 ($string) of type string is deprecated
2022-08-19 15:04:48 +02:00
Guy Sartorelli
f102cad3bf
Merge pull request #64 from creative-commoners/pulls/2.1/phpcs
MNT Add codesniffer
2022-08-05 14:48:24 +12:00
Steve Boyd
d142dd77e4 MNT Add codesniffer 2022-08-05 12:33:00 +12:00
Steve Boyd
a919f8a317 Merge branch '2.1' into 2 2022-08-02 19:11:46 +12:00
Steve Boyd
ea06c57cfb Merge branch '2.0' into 2.1 2022-08-02 19:11:02 +12:00
Guy Sartorelli
d399c27722
Merge pull request #63 from creative-commoners/pulls/2.0/standardise-modules
MNT Standardise modules
2022-08-02 16:18:19 +12:00
Steve Boyd
bf07381fc9 MNT Standardise modules 2022-08-01 16:23:18 +12:00
Guy Sartorelli
2ba8fe5c50
Merge pull request #61 from elliot-sawyer/compat/8.1
PHP 8.1 compatibility
2022-07-25 12:12:09 +12:00
Elliot Sawyer
f9dfa9f34c PHP 8.1 compatibility 2022-07-20 19:17:01 +12:00
Sam Minnée
c95b0105f5
Merge pull request #60 from chrometoasters/pulls/transaction-mode-not-supported
Override transaction mode support check method
2019-09-25 10:03:09 +12:00
Michal Kleiner
22d46a5ef8 Override transaction mode support check method as these are not supported by SQLite 2019-09-16 14:50:16 +12:00
Robbie Averill
10c85d4179 Merge branch '2.1' into 2 2019-01-09 09:35:00 +01:00
Robbie Averill
b5a6416388 Merge branch '2.0' into 2.1 2019-01-09 09:34:48 +01:00
Robbie Averill
4edf7c23f8
Merge pull request #58 from mark-cameron/issue_57_upgrade_connectors_yml_file
Added quotes to values in connectors.yml
2019-01-09 09:33:59 +01:00
Mark Cameron
3c8a06f5b9 Added quotes to values in connectors.yml 2019-01-09 00:17:49 +01:00
Robbie Averill
34a3221097
Merge pull request #52 from silverstripe/features/better-travis-matrix
FIX: Better travis matrix
2018-10-19 11:26:10 +02:00
Sam Minnee
1589089f5b FIX: Better travis matrix
- Test PHP 7.2 and 7.3
 - Test against all 4.x minor releases
2018-10-19 14:21:42 +13:00
Sam Minnée
e919bdffd9
Correct travis badge 2018-10-19 11:54:37 +13:00
Maxime Rainville
40b9e876ba
Merge pull request #51 from NightJar/tighten-transactions
FIX Tighten transactions
2018-10-19 10:47:46 +13:00
NightjarNZ
c2569099ce correct @deprecated docblock to be Draft PSR-5 compliant 2018-10-18 22:12:06 +13:00
NightjarNZ
5eacbe7842 FIX convert index definitions to reflect actual support
It is not uncommon for an index to be defined as e.g. 'fulltext'
which SQLite3 does not support without a module to create a
virtual table (rather than an index on an existing one). The code
already in place sees that definitions be updated to 'index' on
the fly during creation and later inspection (indexList) - which
causes issue when comparing existing table definitions to what
SilverStripe expects by DataObject configuration. This discrepency
leads to tables constantly being marked to update, although
effectively nothing actually changes. We can save these CPU cycles
and a bit of head scratching by converting to a supported index type.
2018-10-16 21:57:51 +13:00
Maxime Rainville
7add192ebf
Merge pull request #50 from NightJar/escape-enum-default-death
FIX preserve enum values with correct escaping
2018-10-12 14:37:08 +13:00
NightjarNZ
0fa6b0fde7 FIX transaction depth related errors with invalid savepoint names
The logic for cancelling a savepoint was incorrect, as the behaviour
the logic was modelled on was for a different RDBMS - where a COMMIT
would always close the most recently opened transaction.

SQLite on contrast will commit the entire transaction, not just the
most recent savepoint marker until current execution point. The correct
manner to deal with a 'partial' commit is to call RELEASE <savepoint>.

This revealed an error in the savepoint logic, in that if someone had
supplied a savepoint name instead of relying on generated ones, the
rollback command did not factor for this and always assumed generated
savepoint names - again causing error. For this reason a new class
member field has been introduced to track savepoint names in a stack
fashion.
2018-10-11 22:07:23 +13:00
NightjarNZ
62ef14f711 FIX correct nesting level mismatches causing errors
Transactions that used more than one level would cause errors if
there were consecutive calls to start a transaction - because each
query executed would clear the flag indicating that a transaction
was already in progress.

The comment for the logic to reset the nesting level on a query was
indicating that DDL (data definition language) would not work within
a transaction. This is untrue, and the module itself uses a transaction
to alter table or field names. So this function has been converted to
a no-op, deprecated to be removed in version 3 of this module. It is
also no longer called upon each query.

There have been some maintenance tidyups around this area also by
abstracting the nested transaction flag manipulations into protected
functions.
2018-10-11 00:02:12 +13:00
NightjarNZ
0efd40e5c2 FIX correct handwritten logic for transactions to use new API instead
Code in the field alteration logic had a queries defiend as strings to
begin and commit transactions involve with changing table or column names.
This was causing fatal errors as BEGIN is not a valid keyword within
a trasaction (see SQLite documentation excerpt below).

A new api has been introduced to deal with transactions programmatically,
and this module was updated to support this a few months ago. This is a
tidy up of some missed portions - consuming this API which correctly uses
SAVEPOINT when a nested transaction is required automatically.

https://www.sqlite.org/lang_transaction.html
Transactions created using BEGIN...COMMIT do not nest. For nested
transactions, use the SAVEPOINT and RELEASE commands.
2018-10-09 22:22:53 +13:00
NightjarNZ
418c1178a1 FIX preserve enum values with correct escaping
Enum values are themselves enumerated in sqlite as they are not supported
as a type. This leads to values being stored in their own table, and a
regular TEXT field being used in a MySQL ENUM's stead. The default value
for this field was being escaped with custom string replacement, and
erroneously relacing the backslash (a redundant operation). This lead
to invalid Fully Qualified Class Names in SilverStripe 4, which is a
required trait for polymorphic relationships. As a result any polymorphic
relationship not set on first write would then proceed to cause an execution
error the next time the dataobject with the relationship was fetched from
the database. By using the PHP supplied escape function for SQLite3 we can
avoid this, and restore functionality.

Relevant section of SQLite documentation to justify the removal of escaping
various characters, such as the backslash:

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.

https://www.sqlite.org/lang_expr.html
2018-10-08 23:09:24 +13:00
Daniel Hensby
4167d9fd1a FIX Make sure nested transactions get reset on implicit commits 2018-07-05 15:31:05 +12:00
Damian Mooyman
6432ceea0d
Merge pull request #46 from creative-commoners/pulls/master/add-supported-module-badge
Add supported module badge to readme
2018-06-18 10:14:43 +12:00
Dylan Wagstaff
d315c61ea0 Add supported module badge to readme 2018-06-15 17:49:06 +12:00
Damian Mooyman
c346e64590
Merge pull request #43 from lekoala/patch-1
return 0 for non iterable results
2018-04-16 09:08:06 +12:00
Damian Mooyman
a38ab53e33
Merge pull request #39 from dhensby/pulls/2.0/nested-transactions
FIX Add nested transaction support
2018-03-13 09:25:09 +13:00
Thomas Portelange
978c371820
return 0 for non iterable results
If there are no columns, it's not a iterable result set and we can return 0. This fixes issues with things like CREATE statement.
2018-03-12 14:04:43 +01:00
Daniel Hensby
b36f3598bb
Merge pull request #40 from mikenz/patch-2
Missing 'n'
2018-02-13 10:50:27 +00:00
Mike Cochrane
34648b9c05
Missing 'n' 2018-02-13 14:18:48 +13:00
Daniel Hensby
f176bb0a39
FIX Add nested transaction support 2018-02-09 11:24:35 +00:00
Damian Mooyman
0e6aa26f55
Merge remote-tracking branch 'origin/2.1' into 2 2017-12-07 16:18:39 +13:00
Damian Mooyman
9b00630616
Merge remote-tracking branch 'origin/2.0' into 2.1
# Conflicts:
#	code/SQLite3Database.php
#	code/SQLiteDatabaseConfigurationHelper.php
2017-12-07 16:18:19 +13:00
Damian Mooyman
e8f4e55b8a
Merge remote-tracking branch 'origin/1.4' into 2.0
# Conflicts:
#	.travis.yml
#	code/SQLite3Database.php
#	composer.json
2017-12-07 16:16:11 +13:00
Damian Mooyman
2bde2640c3
Merge pull request #1 from silverstripe-security/patch/1.4/SS-2017-008
[SS-2017-008] Fix SQL injection in search engine
2017-12-07 15:59:01 +13:00
Damian Mooyman
ca4a76eaab
Update 2 branch alias to 2.2 2017-11-28 10:50:04 +13:00
Damian Mooyman
77e5a5e18c
Merge branch '2.1' into 2 2017-11-28 10:49:33 +13:00
Damian Mooyman
dbf10488ee
Remove master branch alias from 2.1 branch 2017-11-28 10:49:05 +13:00
Daniel Hensby
978a5a19ce
Merge pull request #37 from open-sausages/pulls/4.0/update-styles
Update config / code styles for 4.0
2017-11-23 12:50:04 +00:00
Damian Mooyman
24cf40beaf
Update config / code styles for 4.0
Related https://github.com/silverstripe/silverstripe-framework/issues/7590
2017-11-23 14:11:10 +13:00
Daniel Hensby
4aad42c084
[SS-2017-008] Fix SQL injection in search engine 2017-11-21 16:16:32 +00:00
Damian Mooyman
acaaf95d22 Merge pull request #35 from dhensby/pulls/db-index-update
Update module to work with new stricter index definitions
2017-10-12 14:03:50 +13:00
Daniel Hensby
c00a11cf7e
Travis setup 2017-10-10 15:10:30 +01:00
Daniel Hensby
536ada309e
Update module to work with new stricter index definitions 2017-10-10 13:09:06 +01:00
Damian Mooyman
8d25343c41 Merge pull request #32 from open-sausages/pulls/2/vendorise-me-baby
Expose as vendor module
2017-10-03 16:17:05 +13:00
Ingo Schommer
eba3c2c746 Expose as vendor module 2017-10-03 03:12:27 +13:00
Daniel Hensby
4e854b3fd2 Merge pull request #30 from kinglozzer/fix-seek
FIX: SQLite3Query::seek() failed to return a record
2017-01-13 11:56:18 +00:00
Loz Calver
0a646577fe FIX: SQLite3Query::seek() failed to return a record 2017-01-12 17:08:05 +00:00
Damian Mooyman
47d47ab2a8 Bump alias of master to 2.1 2016-11-13 21:57:58 +13:00
Damian Mooyman
7304708fa1 BUG Fix installer for 4.0 (#29) 2016-10-26 14:24:31 +13:00
Loz Calver
cfdf20b253 Merge pull request #28 from mikenz/patch-1
Update for latest SS4 changes
2016-10-10 09:05:52 +01:00
Mike Cochrane
dd81faac7b Update for latest SS4 changes 2016-10-10 11:13:18 +13:00
Daniel Hensby
360b70aa22 Merge pull request #27 from open-sausages/pulls/4.0/fix-path-warnings
BUG Fix errors when 'path' isn't declared explicitly
2016-09-30 12:24:10 +01:00
Damian Mooyman
6d74fc05cf
BUG Fix errors when 'path' isn't declared explicitly 2016-09-30 15:45:13 +13:00
Damian Mooyman
b710ef04b3 Rename SS_ prefixed classes (#26) 2016-09-09 15:46:48 +12:00
Ingo Schommer
8001e69b71 Merge pull request #25 from open-sausages/pulls/4.0/namespace-everything
Upgrade for silverstripe namespaces
2016-09-08 16:11:21 +12:00
Damian Mooyman
1cb63311d8
Upgrade for silverstripe namespaces 2016-09-08 15:41:17 +12:00
Ingo Schommer
32ae8f8d94 Merge pull request #24 from open-sausages/pulls/4.0/namespace-cms
Update for SilverStripe\CMS namespace
2016-08-12 16:39:36 +12:00
Damian Mooyman
99e6081c42
Update for SilverStripe\CMS namespace 2016-08-12 13:22:58 +12:00
Damian Mooyman
72638cf40e BUG Fix incorrect upgrade / rules 2016-07-05 16:27:01 +12:00
Ingo Schommer
d09ab6bea4 Merge pull request #23 from open-sausages/pulls/4.0/namespace
API Apply SilverStripe\SQLite namespace to module
2016-07-01 15:10:06 +12:00
Damian Mooyman
e36e74ab2f API Apply SilverStripe\SQLite namespace to module 2016-06-29 13:55:45 +12:00
Damian Mooyman
0bd28649f5 Merge pull request #21 from dhensby/pulls/2.0/travis-fix
Update travis for 4.0 tests
2016-03-24 11:57:08 +13:00
Damian Mooyman
df303356cd Merge pull request #19 from helpfulrobot/add-standard-scrutinizer-config
Added standard Scrutinizer config
2016-03-24 11:52:54 +13:00
Loz Calver
149d1708b6 Merge pull request #20 from dhensby/pulls/1.4/travis
Update travis test coverage
2016-03-23 13:50:28 +00:00
Daniel Hensby
8a4e3434bc Update travis for 4.0 tests 2016-03-23 12:48:55 +00:00
Daniel Hensby
ea1d504617 Update travis test coverage 2016-03-23 11:37:24 +00:00
helpfulrobot
c77a73e970 Added standard Scrutinizer config 2016-03-23 23:18:23 +13:00
Daniel Hensby
7cf13c1808 Merge pull request #18 from helpfulrobot/add-standard-code-of-conduct-file
Added standard code of conduct file
2016-02-16 09:44:36 +00:00
helpfulrobot
f7ef5f16de Added standard code of conduct file 2016-02-16 11:42:22 +13:00
Ingo Schommer
9780bb014c Merge pull request #17 from silverstripe-labs/pulls/fix-tests
BUG Fix .travis.yml
2016-01-18 15:54:26 +13:00
Damian Mooyman
c95cbcdc01 BUG Fix .travis.yml 2016-01-18 15:28:10 +13:00
Damian Mooyman
941fc9d857 Merge pull request #16 from helpfulrobot/add-standard-gitattributes-file
Added standard .gitattributes file
2016-01-18 15:25:37 +13:00
helpfulrobot
0c0dceacac Added standard .gitattributes file 2016-01-16 19:32:49 +13:00
Damian Mooyman
608458ccb8 Merge pull request #15 from helpfulrobot/convert-to-psr-2
Converted to PSR-2
2015-12-18 10:05:28 +13:00
helpfulrobot
b2d40ed61e Converted to PSR-2 2015-12-18 07:11:01 +13:00
Damian Mooyman
7daf437e24 Merge pull request #14 from helpfulrobot/add-standard-editorconfig-file
Added standard .editorconfig file
2015-12-17 14:07:08 +13:00
helpfulrobot
4b6dcd8dde Added standard .editorconfig file 2015-12-17 10:06:47 +13:00
Damian Mooyman
e76bd4b1a1 API Update master to new major version for 4.0 compat 2015-10-16 11:17:24 +13:00
22 changed files with 2168 additions and 1611 deletions

17
.editorconfig Normal file
View File

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

1
.gitattributes vendored Normal file
View File

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

11
.github/workflows/ci.yml vendored Normal file
View File

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

16
.github/workflows/dispatch-ci.yml vendored Normal file
View File

@ -0,0 +1,16 @@
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

17
.github/workflows/keepalive.yml vendored Normal file
View File

@ -0,0 +1,17 @@
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,20 +0,0 @@
language: php
sudo: false
php:
- 5.3
env:
matrix:
- DB=POSTGRESQL CORE_RELEASE=master
before_script:
- composer self-update || true
- phpenv rehash
- git clone git://github.com/silverstripe-labs/silverstripe-travis-support.git ~/travis-support
- php ~/travis-support/travis_setup.php --source `pwd` --target ~/builds/ss
- cd ~/builds/ss
script:
- phpunit framework/tests

7
.upgrade.yml Normal file
View File

@ -0,0 +1,7 @@
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

View File

@ -1,6 +1,7 @@
# SQLite3 Module # SQLite3 Module
[![Build Status](https://travis-ci.org/silverstripe-labs/silverstripe-sqlite3.png?branch=master)](https://travis-ci.org/silverstripe-labs/silverstripe-sqlite3) [![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 ## Maintainer Contact
@ -9,26 +10,23 @@ Andreas Piening (Nickname: apiening)
## Requirements ## Requirements
* SilverStripe 3.2 or newer * Silverstripe 4.0 or newer
## Installation ## Installation
* If using composer, run `composer require silverstripe/sqlite3 1.4.*-dev`. * Install using composer with `composer require silverstripe/sqlite3 ^2`.
* Otherwise, download, unzip and copy the sqlite3 folder to your project root so that it becomes a
sibling of `framework/`.
## Configuration ## Configuration
Either use the installer to automatically install SQLite or add this to your _config.php (right after 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) "require_once("conf/ConfigureFromEnv.php");" if you are using _ss_environment.php)
$databaseConfig['type'] = 'SQLiteDatabase'; $databaseConfig['type'] = 'SQLite3Database';
$databaseConfig['path'] = "/path/to/my/database/file"; $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 Make sure the webserver has sufficient privileges to write to that folder and that it is protected from
external access. external access.
### Sample mysite/_config.php ### Sample mysite/_config.php
```php ```php
@ -42,18 +40,14 @@ $database = 'SS_mysite';
require_once("conf/ConfigureFromEnv.php"); require_once("conf/ConfigureFromEnv.php");
global $databaseConfig; global $databaseConfig;
$databaseConfig = array( $databaseConfig = array(
"type" => 'SQLiteDatabase', "type" => 'SQLite3Database',
"server" => 'none', "server" => 'none',
"username" => 'none', "username" => 'none',
"password" => 'none', "password" => 'none',
"database" => $database, "database" => $database,
"path" => "/path/to/my/database/file", "path" => "/path/to/my/database/file",
); );
SSViewer::set_theme('blackcandy');
SiteTree::enable_nested_urls();
``` ```
Again: make sure that the webserver has permission to read and write to the above path (/path/to/my/database/, Again: make sure that the webserver has permission to read and write to the above path (/path/to/my/database/,

View File

@ -1,3 +1 @@
<?php <?php
Deprecation::notification_version('1.4.0', 'sqlite3');

View File

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

View File

@ -1,4 +0,0 @@
SQLLite3Database:
# Extension used to distinguish between sqllite database files and other files
# Required to handle multiple databases
database_extension: '.sqlite'

View File

@ -1,14 +1,25 @@
<?php <?php
// Script called from ConfigureFromEnv.php // Called from DatabaseAdapterRegistry::autoconfigure($config)
global $databaseConfig; use SilverStripe\Core\Environment;
if(strpos($databaseConfig['type'], 'SQLite') === 0) { use SilverStripe\SQLite\SQLite3Database;
if(defined('SS_SQLITE_DATABASE_PATH')) { if (!isset($databaseConfig)) {
$databaseConfig['path'] = SS_SQLITE_DATABASE_PATH; global $databaseConfig;
} }
if(defined('SS_SQLITE_DATABASE_KEY')) { // Get path
$databaseConfig['key'] = SS_SQLITE_DATABASE_KEY; $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,45 +1,54 @@
<?php <?php
use SilverStripe\Dev\Install\DatabaseAdapterRegistry;
use SilverStripe\SQLite\SQLiteDatabaseConfigurationHelper;
$sqliteDatabaseAdapterRegistryFields = array( $sqliteDatabaseAdapterRegistryFields = array(
'path' => array( 'path' => array(
'title' => 'Directory path<br /><small>Absolute path to directory, writeable by the webserver user.<br />' 'title' => 'Directory path<br /><small>Absolute path to directory, writeable by the webserver user.<br />'
. 'Recommended to be outside of your webroot</small>', . 'Recommended to be outside of your webroot</small>',
'default' => dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'assets' . DIRECTORY_SEPARATOR . '.sqlitedb' 'default' => dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'assets' . DIRECTORY_SEPARATOR . '.sqlitedb'
), ),
'database' => array( 'database' => array(
'title' => 'Database filename (extension .sqlite)', 'title' => 'Database filename (extension .sqlite)',
'default' => 'database.sqlite' 'default' => 'database.sqlite'
) )
); );
// Basic SQLLite3 Database // Basic SQLLite3 Database
/** @skipUpgrade */
DatabaseAdapterRegistry::register( DatabaseAdapterRegistry::register(
array( array(
'class' => 'SQLite3Database', 'class' => 'SQLite3Database',
'title' => 'SQLite 3.3+ (using SQLite3)', 'module' => 'sqlite3',
'helperPath' => dirname(__FILE__).'/code/SQLiteDatabaseConfigurationHelper.php', 'title' => 'SQLite 3.3+ (using SQLite3)',
'supported' => class_exists('SQLite3'), 'helperPath' => __DIR__.'/code/SQLiteDatabaseConfigurationHelper.php',
'missingExtensionText' => 'The <a href="http://php.net/manual/en/book.sqlite3.php">SQLite3</a> '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.', PHP Extension is not available. Please install or enable it of them and refresh this page.',
'fields' => array_merge($sqliteDatabaseAdapterRegistryFields, array('key' => array( 'fields' => array_merge($sqliteDatabaseAdapterRegistryFields, array('key' => array(
'title' => 'Encryption key<br><small>This function is experimental and requires configuration of an ' 'title' => 'Encryption key<br><small>This function is experimental and requires configuration of an '
. 'encryption module</small>', . 'encryption module</small>',
'default' => '' 'default' => ''
))) )))
) )
); );
// PDO database // PDO database
/** @skipUpgrade */
DatabaseAdapterRegistry::register( DatabaseAdapterRegistry::register(
array( array(
'class' => 'SQLite3PDODatabase', 'class' => 'SQLite3PDODatabase',
'title' => 'SQLite 3.3+ (using PDO)', 'module' => 'sqlite3',
'helperPath' => dirname(__FILE__).'/code/SQLiteDatabaseConfigurationHelper.php', 'title' => 'SQLite 3.3+ (using PDO)',
'supported' => (class_exists('PDO') && in_array('sqlite', PDO::getAvailableDrivers())), 'helperPath' => __DIR__.'/code/SQLiteDatabaseConfigurationHelper.php',
'missingExtensionText' => 'helperClass' => SQLiteDatabaseConfigurationHelper::class,
'Either the <a href="http://php.net/manual/en/book.pdo.php">PDO Extension</a> or the '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> <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.', are unavailable. Please install or enable these and refresh this page.',
'fields' => $sqliteDatabaseAdapterRegistryFields 'fields' => $sqliteDatabaseAdapterRegistryFields
) )
); );

1
code-of-conduct.md Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,615 +1,741 @@
<?php <?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 * SQLite schema manager class
*
* @package SQLite3
*/ */
class SQLite3SchemaManager extends DBSchemaManager { class SQLite3SchemaManager extends DBSchemaManager
{
/** /**
* Instance of the database controller this schema belongs to * Instance of the database controller this schema belongs to
* *
* @var SQLite3Database * @var SQLite3Database
*/ */
protected $database = null; protected $database = null;
/** /**
* Flag indicating whether or not the database has been checked and repaired * Flag indicating whether or not the database has been checked and repaired
* *
* @var boolean * @var boolean
*/ */
protected static $checked_and_repaired = false; protected static $checked_and_repaired = false;
/** /**
* Should schema be vacuumed during checkeAndRepairTable? * Should schema be vacuumed during checkeAndRepairTable?
* *
* @var boolean * @var boolean
*/ */
public static $vacuum = true; public static $vacuum = true;
public function createDatabase($name) { public function createDatabase($name)
// Ensure that any existing database is cleared before connection {
$this->dropDatabase($name); // Ensure that any existing database is cleared before connection
} $this->dropDatabase($name);
}
public function dropDatabase($name) { public function dropDatabase($name)
// No need to delete database files if operating purely within memory {
if($this->database->getLivesInMemory()) return; // 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 // If using file based database ensure any existing file is removed
$parameters = $this->database->getParameters(); $path = $this->database->getPath();
$fullpath = $parameters['path'] . '/' . $name; $fullpath = $path . '/' . $name . SQLite3Database::database_extension();
if(is_writable($fullpath)) unlink($fullpath); if (is_writable($fullpath)) {
} unlink($fullpath);
}
}
function databaseList() { public function databaseList()
$parameters = $this->database->getParameters(); {
// If in-memory use the current database name only
if ($this->database->getLivesInMemory()) {
return array(
$this->database->getConnector()->getSelectedDatabase()
?: 'database'
);
}
// If in-memory use the current database name only // If using file based database enumerate files in the database directory
if($this->database->getLivesInMemory()) { $directory = $this->database->getPath();
return array($parameters['database']); $files = scandir($directory);
}
// If using file based database enumerate files in the database directory // Filter each file in this directory
$directory = $parameters['path']; $databases = array();
$files = scandir($directory); if ($files !== false) {
foreach ($files as $file) {
// Filter non-files
if (!is_file("$directory/$file")) {
continue;
}
// Filter each file in this directory // Filter those with correct extension
$databases = array(); if (!SQLite3Database::is_valid_database_name($file)) {
if($files !== false) foreach($files as $file) { continue;
}
// Filter non-files if ($extension = SQLite3Database::database_extension()) {
if(!is_file("$directory/$file")) continue; $databases[] = substr($file, 0, -strlen($extension));
} else {
$databases[] = $file;
}
}
}
return $databases;
}
// Filter those with correct extension public function databaseExists($name)
if(!SQLite3Database::is_valid_database_name($file)) continue; {
$databases = $this->databaseList();
return in_array($name, $databases);
}
$databases[] = $file; /**
} * Empties any cached enum values
return $databases; */
} public function flushCache()
{
$this->enum_map = array();
}
public function databaseExists($name) { public function schemaUpdate($callback)
$databases = $this->databaseList(); {
return in_array($name, $databases); // Set locking mode
} $this->database->setPragma('locking_mode', 'EXCLUSIVE');
$this->checkAndRepairTable();
$this->flushCache();
/** // Initiate schema update
* Empties any cached enum values $error = null;
*/ try {
public function flushCache() { parent::schemaUpdate($callback);
$this->enum_map = array(); } catch (Exception $ex) {
} $error = $ex;
}
function schemaUpdate($callback) { // Revert locking mode
// Set locking mode $this->database->setPragma('locking_mode', SQLite3Database::$default_pragma['locking_mode']);
$this->database->setPragma('locking_mode', 'EXCLUSIVE');
$this->checkAndRepairTable();
$this->flushCache();
// Initiate schema update if ($error) {
$error = null; throw $error;
try { }
parent::schemaUpdate($callback); }
} catch(Exception $ex) {
$error = $ex;
}
// Revert locking mode /**
$this->database->setPragma('locking_mode', SQLite3Database::$default_pragma['locking_mode']); * Empty a specific table
*
* @param string $table
*/
public function clearTable($table)
{
if ($table != 'SQLiteEnums') {
$this->query("DELETE FROM \"$table\"");
}
}
if($error) throw $error; public function createTable($table, $fields = null, $indexes = null, $options = null, $advancedOptions = null)
} {
if (!isset($fields['ID'])) {
$fields['ID'] = $this->IdColumn();
}
/** $fieldSchemata = array();
* Empty a specific table if ($fields) {
* foreach ($fields as $k => $v) {
* @param string $table $fieldSchemata[] = "\"$k\" $v";
*/ }
public function clearTable($table) { }
if($table != 'SQLiteEnums') $this->dbConn->query("DELETE FROM \"$table\""); $fieldSchemas = implode(",\n", $fieldSchemata);
}
public function createTable($table, $fields = null, $indexes = null, $options = null, $advancedOptions = null) { // Switch to "CREATE TEMPORARY TABLE" for temporary tables
if(!isset($fields['ID'])) $fields['ID'] = $this->IdColumn(); $temporary = empty($options['temporary']) ? "" : "TEMPORARY";
$this->query("CREATE $temporary TABLE \"$table\" (
$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 $fieldSchemas
)"); )");
if($indexes) { if ($indexes) {
foreach($indexes as $indexName => $indexDetails) { foreach ($indexes as $indexName => $indexDetails) {
$this->createIndex($table, $indexName, $indexDetails); $this->createIndex($table, $indexName, $indexDetails);
} }
} }
return $table; return $table;
} }
public function alterTable($tableName, $newFields = null, $newIndexes = null, $alteredFields = null, public function alterTable(
$alteredIndexes = null, $alteredOptions = null, $advancedOptions = null $tableName,
) { $newFields = null,
if($newFields) foreach($newFields as $fieldName => $fieldSpec) { $newIndexes = null,
$this->createField($tableName, $fieldName, $fieldSpec); $alteredFields = null,
} $alteredIndexes = null,
$alteredOptions = null,
if($alteredFields) foreach($alteredFields as $fieldName => $fieldSpec) { $advancedOptions = null
$this->alterField($tableName, $fieldName, $fieldSpec); ) {
} if ($newFields) {
foreach ($newFields as $fieldName => $fieldSpec) {
if($newIndexes) foreach($newIndexes as $indexName => $indexSpec) { $this->createField($tableName, $fieldName, $fieldSpec);
$this->createIndex($tableName, $indexName, $indexSpec); }
} }
if($alteredIndexes) foreach($alteredIndexes as $indexName => $indexSpec) { if ($alteredFields) {
$this->alterIndex($tableName, $indexName, $indexSpec); foreach ($alteredFields as $fieldName => $fieldSpec) {
} $this->alterField($tableName, $fieldName, $fieldSpec);
} }
}
public function renameTable($oldTableName, $newTableName) {
$this->query("ALTER TABLE \"$oldTableName\" RENAME TO \"$newTableName\""); if ($newIndexes) {
} foreach ($newIndexes as $indexName => $indexSpec) {
$this->createIndex($tableName, $indexName, $indexSpec);
public function checkAndRepairTable($tableName = null) { }
$ok = true; }
if(!SapphireTest::using_temp_db() && !self::$checked_and_repaired) { if ($alteredIndexes) {
$this->alterationMessage("Checking database integrity", "repaired"); foreach ($alteredIndexes as $indexName => $indexSpec) {
$this->alterIndex($tableName, $indexName, $indexSpec);
// 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; public function renameTable($oldTableName, $newTableName)
} {
} $this->query("ALTER TABLE \"$oldTableName\" RENAME TO \"$newTableName\"");
}
// If enabled vacuum (clean and rebuild) the database
if(self::$vacuum) { public function checkAndRepairTable($tableName = null)
$this->query('VACUUM', E_USER_NOTICE); {
$message = $this->database->getConnector()->getLastError(); $ok = true;
if(preg_match('/authoriz/', $message)) {
$this->alterationMessage("VACUUM | $message", "error"); if (!self::$checked_and_repaired) {
} else { $this->alterationMessage("Checking database integrity", "repaired");
$this->alterationMessage("VACUUMing", "repaired");
} // Check for any tables with failed integrity
} if ($messages = $this->query('PRAGMA integrity_check')) {
self::$checked_and_repaired = true; foreach ($messages as $message) {
} if ($message['integrity_check'] != 'ok') {
Debug::show($message['integrity_check']);
return $ok; $ok = false;
} }
}
public function createField($table, $field, $spec) { }
$this->query("ALTER TABLE \"$table\" ADD \"$field\" $spec");
} // If enabled vacuum (clean and rebuild) the database
if (self::$vacuum) {
/** $this->query('VACUUM', E_USER_NOTICE);
* Change the database type of the given field. $message = $this->database->getConnector()->getLastError();
* @param string $tableName The name of the tbale the field is in. if (preg_match('/authoriz/', $message ?? '')) {
* @param string $fieldName The name of the field to change. $this->alterationMessage("VACUUM | $message", "error");
* @param string $fieldSpec The new field specification } else {
*/ $this->alterationMessage("VACUUMing", "repaired");
public function alterField($tableName, $fieldName, $fieldSpec) { }
$oldFieldList = $this->fieldList($tableName); }
$fieldNameList = '"' . implode('","', array_keys($oldFieldList)) . '"'; self::$checked_and_repaired = true;
}
if(!empty($_REQUEST['avoidConflict']) && Director::isDev()) {
$fieldSpec = preg_replace('/\snot null\s/i', ' NOT NULL ON CONFLICT REPLACE ', $fieldSpec); return $ok;
} }
// Skip non-existing columns public function createField($table, $field, $spec)
if(!array_key_exists($fieldName, $oldFieldList)) return; {
$this->query("ALTER TABLE \"$table\" ADD \"$field\" $spec");
// Update field spec }
$newColsSpec = array();
foreach($oldFieldList as $name => $oldSpec) { /**
$newColsSpec[] = "\"$name\" " . ($name == $fieldName ? $fieldSpec : $oldSpec); * 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.
$queries = array( * @param string $fieldSpec The new field specification
"BEGIN TRANSACTION", */
"CREATE TABLE \"{$tableName}_alterfield_{$fieldName}\"(" . implode(',', $newColsSpec) . ")", public function alterField($tableName, $fieldName, $fieldSpec)
"INSERT INTO \"{$tableName}_alterfield_{$fieldName}\" SELECT {$fieldNameList} FROM \"$tableName\"", {
"DROP TABLE \"$tableName\"", $oldFieldList = $this->fieldList($tableName);
"ALTER TABLE \"{$tableName}_alterfield_{$fieldName}\" RENAME TO \"$tableName\"", $fieldNameList = '"' . implode('","', array_keys($oldFieldList)) . '"';
"COMMIT"
); if (!empty($_REQUEST['avoidConflict']) && Director::isDev()) {
$fieldSpec = preg_replace('/\snot null\s/i', ' NOT NULL ON CONFLICT REPLACE ', $fieldSpec);
// Remember original indexes }
$indexList = $this->indexList($tableName);
// Skip non-existing columns
// Then alter the table column if (!array_key_exists($fieldName, $oldFieldList)) {
foreach($queries as $query) $this->query($query.';'); return;
}
// Recreate the indexes
foreach($indexList as $indexName => $indexSpec) { // Update field spec
$this->createIndex($tableName, $indexName, $indexSpec); $newColsSpec = array();
} foreach ($oldFieldList as $name => $oldSpec) {
} $newColsSpec[] = "\"$name\" " . ($name == $fieldName ? $fieldSpec : $oldSpec);
}
public function renameField($tableName, $oldName, $newName) {
$oldFieldList = $this->fieldList($tableName); $queries = array(
"CREATE TABLE \"{$tableName}_alterfield_{$fieldName}\"(" . implode(',', $newColsSpec) . ")",
// Skip non-existing columns "INSERT INTO \"{$tableName}_alterfield_{$fieldName}\" SELECT {$fieldNameList} FROM \"$tableName\"",
if(!array_key_exists($oldName, $oldFieldList)) return; "DROP TABLE \"$tableName\"",
"ALTER TABLE \"{$tableName}_alterfield_{$fieldName}\" RENAME TO \"$tableName\"",
// Determine column mappings );
$oldCols = array();
$newColsSpec = array(); // Remember original indexes
foreach($oldFieldList as $name => $spec) { $indexList = $this->indexList($tableName);
$oldCols[] = "\"$name\"" . (($name == $oldName) ? " AS $newName" : '');
$newColsSpec[] = "\"" . (($name == $oldName) ? $newName : $name) . "\" $spec"; // Then alter the table column
} $database = $this->database;
$database->withTransaction(function () use ($database, $queries, $indexList) {
// SQLite doesn't support direct renames through ALTER TABLE foreach ($queries as $query) {
$queries = array( $database->query($query . ';');
"BEGIN TRANSACTION", }
"CREATE TABLE \"{$tableName}_renamefield_{$oldName}\" (" . implode(',', $newColsSpec) . ")", });
"INSERT INTO \"{$tableName}_renamefield_{$oldName}\" SELECT " . implode(',', $oldCols) . " FROM \"$tableName\"",
"DROP TABLE \"$tableName\"", // Recreate the indexes
"ALTER TABLE \"{$tableName}_renamefield_{$oldName}\" RENAME TO \"$tableName\"", foreach ($indexList as $indexName => $indexSpec) {
"COMMIT" $this->createIndex($tableName, $indexName, $indexSpec);
); }
}
// Remember original indexes
$oldIndexList = $this->indexList($tableName); public function renameField($tableName, $oldName, $newName)
{
// Then alter the table column $oldFieldList = $this->fieldList($tableName);
foreach($queries as $query) $this->query($query.';');
// Skip non-existing columns
// Recreate the indexes if (!array_key_exists($oldName, $oldFieldList)) {
foreach($oldIndexList as $indexName => $indexSpec) { return;
// Rename columns to new columns }
$indexSpec['value'] = preg_replace("/\"$oldName\"/i", "\"$newName\"", $indexSpec['value']);
$this->createIndex($tableName, $indexName, $indexSpec); // Determine column mappings
} $oldCols = array();
} $newColsSpec = array();
foreach ($oldFieldList as $name => $spec) {
public function fieldList($table) { $oldCols[] = "\"$name\"" . (($name == $oldName) ? " AS $newName" : '');
$sqlCreate = $this->preparedQuery( $newColsSpec[] = "\"" . (($name == $oldName) ? $newName : $name) . "\" $spec";
'SELECT sql FROM sqlite_master WHERE type = ? AND name = ?', }
array('table', $table)
)->record(); // SQLite doesn't support direct renames through ALTER TABLE
$oldColsStr = implode(',', $oldCols);
$fieldList = array(); $newColsSpecStr = implode(',', $newColsSpec);
if($sqlCreate && $sqlCreate['sql']) { $queries = array(
preg_match('/^[\s]*CREATE[\s]+TABLE[\s]+[\'"]?[a-zA-Z0-9_\\\]+[\'"]?[\s]*\((.+)\)[\s]*$/ims', "CREATE TABLE \"{$tableName}_renamefield_{$oldName}\" ({$newColsSpecStr})",
$sqlCreate['sql'], $matches "INSERT INTO \"{$tableName}_renamefield_{$oldName}\" SELECT {$oldColsStr} FROM \"$tableName\"",
); "DROP TABLE \"$tableName\"",
$fields = isset($matches[1]) "ALTER TABLE \"{$tableName}_renamefield_{$oldName}\" RENAME TO \"$tableName\"",
? preg_split('/,(?=(?:[^\'"]*$)|(?:[^\'"]*[\'"][^\'"]*[\'"][^\'"]*)*$)/x', $matches[1]) );
: array();
foreach($fields as $field) { // Remember original indexes
$details = preg_split('/\s/', trim($field)); $oldIndexList = $this->indexList($tableName);
$name = array_shift($details);
$name = str_replace('"', '', trim($name)); // Then alter the table column
$fieldList[$name] = implode(' ', $details); $database = $this->database;
} $database->withTransaction(function () use ($database, $queries) {
} foreach ($queries as $query) {
return $fieldList; $database->query($query . ';');
} }
});
/**
* Create an index on a table. // Recreate the indexes
* foreach ($oldIndexList as $indexName => $indexSpec) {
* @param string $tableName The name of the table. // Map index columns
* @param string $indexName The name of the index. $columns = array_filter(array_map(function ($column) use ($newName, $oldName) {
* @param array $indexSpec The specification of the index, see Database::requireIndex() for more details. // Unchanged
*/ if ($column !== $oldName) {
public function createIndex($tableName, $indexName, $indexSpec) { return $column;
$parsedSpec = $this->parseIndexSpec($indexName, $indexSpec); }
$sqliteName = $this->buildSQLiteIndexName($tableName, $indexName); // Skip obsolete fields
$columns = $parsedSpec['value']; if (stripos($newName, '_obsolete_') === 0) {
$unique = ($parsedSpec['type'] == 'unique') ? 'UNIQUE' : ''; return null;
$this->query("CREATE $unique INDEX IF NOT EXISTS \"$sqliteName\" ON \"$tableName\" ($columns)"); }
} return $newName;
}, $indexSpec['columns']));
public function alterIndex($tableName, $indexName, $indexSpec) {
// Drop existing index // Create index if column count unchanged
$sqliteName = $this->buildSQLiteIndexName($tableName, $indexName); if (count($columns) === count($indexSpec['columns'])) {
$this->query("DROP INDEX IF EXISTS \"$sqliteName\""); $indexSpec['columns'] = $columns;
$this->createIndex($tableName, $indexName, $indexSpec);
// Create the index }
$this->createIndex($tableName, $indexName, $indexSpec); }
} }
/** public function fieldList($table)
* Builds the internal SQLLite index name given the silverstripe table and index name. {
* $sqlCreate = $this->preparedQuery(
* The name is built using the table and index name in order to prevent name collisions 'SELECT "sql" FROM "sqlite_master" WHERE "type" = ? AND "name" = ?',
* between indexes of the same name across multiple tables array('table', $table)
* )->record();
* @param string $tableName
* @param string $indexName $fieldList = array();
* @return string The SQLite3 name of the index if ($sqlCreate && $sqlCreate['sql']) {
*/ preg_match(
protected function buildSQLiteIndexName($tableName, $indexName) { '/^[\s]*CREATE[\s]+TABLE[\s]+[\'"]?[a-zA-Z0-9_\\\]+[\'"]?[\s]*\((.+)\)[\s]*$/ims',
return "{$tableName}_{$indexName}"; $sqlCreate['sql'] ?? '',
} $matches
);
protected function parseIndexSpec($name, $spec) { $fields = isset($matches[1])
$spec = parent::parseIndexSpec($name, $spec); ? preg_split('/,(?=(?:[^\'"]*$)|(?:[^\'"]*[\'"][^\'"]*[\'"][^\'"]*)*$)/x', $matches[1])
: array();
// Only allow index / unique index types foreach ($fields as $field) {
if(!in_array($spec['type'], array('index', 'unique'))) { $details = preg_split('/\s/', trim($field));
$spec['type'] = 'index'; $name = array_shift($details);
} $name = str_replace('"', '', trim($name));
$fieldList[$name] = implode(' ', $details);
return $spec; }
} }
return $fieldList;
public function indexKey($table, $index, $spec) { }
return $this->buildSQLiteIndexName($table, $index);
} /**
* Create an index on a table.
public function indexList($table) { *
$indexList = array(); * @param string $tableName The name of the table.
* @param string $indexName The name of the index.
// Enumerate each index and related fields * @param array $indexSpec The specification of the index, see Database::requireIndex() for more details.
foreach($this->query("PRAGMA index_list(\"$table\")") as $index) { */
public function createIndex($tableName, $indexName, $indexSpec)
// The SQLite internal index name, not the actual Silverstripe name {
$indexName = $index["name"]; $sqliteName = $this->buildSQLiteIndexName($tableName, $indexName);
$indexType = $index['unique'] ? 'unique' : 'index'; $columns = $this->implodeColumnList($indexSpec['columns']);
$unique = ($indexSpec['type'] == 'unique') ? 'UNIQUE' : '';
// Determine a clean list of column names within this index $this->query("CREATE $unique INDEX IF NOT EXISTS \"$sqliteName\" ON \"$tableName\" ($columns)");
$list = array(); }
foreach($this->query("PRAGMA index_info(\"$indexName\")") as $details) {
$list[] = preg_replace('/^"?(.*)"?$/', '$1', $details['name']); public function alterIndex($tableName, $indexName, $indexSpec)
} {
// Drop existing index
// Safely encode this spec $sqliteName = $this->buildSQLiteIndexName($tableName, $indexName);
$indexList[$indexName] = $this->parseIndexSpec($indexName, array( $this->query("DROP INDEX IF EXISTS \"$sqliteName\"");
'name' => $indexName,
'value' => $this->implodeColumnList($list), // Create the index
'type' => $indexType $this->createIndex($tableName, $indexName, $indexSpec);
)); }
}
/**
return $indexList; * 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
public function tableList() { * between indexes of the same name across multiple tables
$tables = array(); *
$result = $this->preparedQuery('SELECT name FROM sqlite_master WHERE type = ?', array('table')); * @param string $tableName
foreach($result as $record) { * @param string $indexName
$table = reset($record); * @return string The SQLite3 name of the index
$tables[strtolower($table)] = $table; */
} protected function buildSQLiteIndexName($tableName, $indexName)
return $tables; {
} return "{$tableName}_{$indexName}";
}
/**
* Return a boolean type-formatted string public function indexKey($table, $index, $spec)
* {
* @params array $values Contains a tokenised list of info about this data type return $this->buildSQLiteIndexName($table, $index);
* @return string }
*/
public function boolean($values) { protected function convertIndexSpec($indexSpec)
$default = empty($values['default']) ? 0 : (int)$values['default']; {
return "BOOL NOT NULL DEFAULT $default"; $supportedIndexTypes = ['index', 'unique'];
} if (isset($indexSpec['type']) && !in_array($indexSpec['type'], $supportedIndexTypes)) {
$indexSpec['type'] = 'index';
/** }
* Return a date type-formatted string return parent::convertIndexSpec($indexSpec);
* }
* @params array $values Contains a tokenised list of info about this data type
* @return string public function indexList($table)
*/ {
public function date($values){ $indexList = array();
return "TEXT";
} // 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
* Return a decimal type-formatted string $indexName = $index["name"];
* $indexType = $index['unique'] ? 'unique' : 'index';
* @params array $values Contains a tokenised list of info about this data type
* @return string // Determine a clean list of column names within this index
*/ $list = array();
public function decimal($values, $asDbValue = false) { foreach ($this->query("PRAGMA index_info(\"$indexName\")") as $details) {
$default = isset($values['default']) && is_numeric($values['default']) ? $values['default'] : 0; $list[] = preg_replace('/^"?(.*)"?$/', '$1', $details['name']);
return "NUMERIC NOT NULL DEFAULT $default"; }
}
// Safely encode this spec
/** $indexList[$indexName] = array(
* Cached list of enum values indexed by table.column 'name' => $indexName,
* 'columns' => $list,
* @var array 'type' => $indexType,
*/ );
protected $enum_map = array(); }
/** return $indexList;
* Return a enum type-formatted string }
*
* enums are not supported. as a workaround to store allowed values we creates an additional table public function tableList()
* {
* @params array $values Contains a tokenised list of info about this data type $tables = array();
* @return string $result = $this->preparedQuery('SELECT name FROM sqlite_master WHERE type = ?', array('table'));
*/ foreach ($result as $record) {
public function enum($values){ $table = reset($record);
$tablefield = $values['table'] . '.' . $values['name']; $tables[strtolower($table)] = $table;
$enumValues = implode(',', $values['enums']); }
return $tables;
// Ensure the cache table exists }
if(empty($this->enum_map)) {
$this->query("CREATE TABLE IF NOT EXISTS \"SQLiteEnums\" (\"TableColumn\" TEXT PRIMARY KEY, \"EnumList\" TEXT)"); /**
} * Return a boolean type-formatted string
*
// Ensure the table row exists * @param array $values Contains a tokenised list of info about this data type
if(empty($this->enum_map[$tablefield]) || $this->enum_map[$tablefield] != $enumValues) { * @return string
$this->preparedQuery( */
"REPLACE INTO SQLiteEnums (TableColumn, EnumList) VALUES (?, ?)", public function boolean($values)
array($tablefield, $enumValues) {
); $default = empty($values['default']) ? 0 : (int)$values['default'];
$this->enum_map[$tablefield] = $enumValues; return "BOOL NOT NULL DEFAULT $default";
} }
// Set default /**
if(!empty($values['default'])) { * Return a date type-formatted string
$default = str_replace(array('"',"'","\\","\0"), "", $values['default']); *
return "TEXT DEFAULT '$default'"; * @param array $values Contains a tokenised list of info about this data type
} else { * @return string
return 'TEXT'; */
} public function date($values)
} {
return "TEXT";
/** }
* Return a set type-formatted string
* This type doesn't exist in SQLite either /**
* * Return a decimal type-formatted string
* @see SQLite3SchemaManager::enum() *
* * @param array $values Contains a tokenised list of info about this data type
* @params array $values Contains a tokenised list of info about this data type * @return string
* @return string */
*/ public function decimal($values)
public function set($values) { {
return $this->enum($values); $default = isset($values['default']) && is_numeric($values['default']) ? $values['default'] : 0;
} return "NUMERIC NOT NULL DEFAULT $default";
}
/**
* Return a float type-formatted string /**
* * Cached list of enum values indexed by table.column
* @params array $values Contains a tokenised list of info about this data type *
* @return string * @var array
*/ */
public function float($values, $asDbValue = false){ protected $enum_map = array();
return "REAL";
} /**
* Return a enum type-formatted string
/** *
* Return a Double type-formatted string * enums 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 * @param array $values Contains a tokenised list of info about this data type
* @return string * @return string
*/ */
public function double($values, $asDbValue = false){ public function enum($values)
return "REAL"; {
} $tablefield = $values['table'] . '.' . $values['name'];
$enumValues = implode(',', $values['enums']);
/**
* Return a int type-formatted string // Ensure the cache table exists
* if (empty($this->enum_map)) {
* @params array $values Contains a tokenised list of info about this data type $this->query(
* @return string "CREATE TABLE IF NOT EXISTS \"SQLiteEnums\" (\"TableColumn\" TEXT PRIMARY KEY, \"EnumList\" TEXT)"
*/ );
public function int($values, $asDbValue = false){ }
return "INTEGER({$values['precision']}) " . strtoupper($values['null']) . " DEFAULT " . (int)$values['default'];
} // Ensure the table row exists
if (empty($this->enum_map[$tablefield]) || $this->enum_map[$tablefield] != $enumValues) {
/** $this->preparedQuery(
* Return a bigint type-formatted string "REPLACE INTO SQLiteEnums (TableColumn, EnumList) VALUES (?, ?)",
* array($tablefield, $enumValues)
* @params array $values Contains a tokenised list of info about this data type );
* @return string $this->enum_map[$tablefield] = $enumValues;
*/ }
public function bigint($values, $asDbValue = false){
return $this->int($values, $asDbValue); // Set default
} if (!empty($values['default'])) {
/*
/** On escaping strings:
* Return a datetime type-formatted string
* For SQLite3, we simply return the word 'TEXT', no other parameters are necessary https://www.sqlite.org/lang_expr.html
* "A string constant is formed by enclosing the string in single quotes ('). A single quote within
* @params array $values Contains a tokenised list of info about this data type the string can be encoded by putting two single quotes in a row - as in Pascal. C-style escapes
* @return string using the backslash character are not supported because they are not standard SQL."
*/
public function ss_datetime($values, $asDbValue = false) { Also, there is a nifty PHP function for this. However apparently one must still be cautious of
return "DATETIME"; 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'";
* Return a text type-formatted string } else {
* return 'TEXT';
* @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 set type-formatted string
} * This type doesn't exist in SQLite either
*
/** * @see SQLite3SchemaManager::enum()
* Return a time type-formatted string *
* * @param array $values Contains a tokenised list of info about this data type
* @params array $values Contains a tokenised list of info about this data type * @return string
* @return string */
*/ public function set($values)
public function time($values) { {
return "TEXT"; return $this->enum($values);
} }
/** /**
* Return a varchar type-formatted string * Return a float type-formatted string
* *
* @params array $values Contains a tokenised list of info about this data type * @param array $values Contains a tokenised list of info about this data type
* @return string * @return string
*/ */
public function varchar($values, $asDbValue = false) { public function float($values)
return "VARCHAR({$values['precision']}) COLLATE NOCASE"; {
} return "REAL";
}
/*
* Return a 4 digit numeric type. MySQL has a proprietary 'Year' type. /**
* For SQLite3 we use TEXT * Return a Double type-formatted string
*/ *
public function year($values, $asDbValue = false){ * @param array $values Contains a tokenised list of info about this data type
return "TEXT"; * @return string
} */
public function double($values)
public function IdColumn($asDbValue = false, $hasAutoIncPK = true){ {
return 'INTEGER PRIMARY KEY AUTOINCREMENT'; return "REAL";
} }
function hasTable($tableName) { /**
return (bool)$this->preparedQuery( * Return a int type-formatted string
'SELECT name FROM sqlite_master WHERE type = ? AND name = ?', *
array('table', $tableName) * @param array $values Contains a tokenised list of info about this data type
)->first(); * @return string
} */
public function int($values)
/** {
* Return enum values for the given field return "INTEGER({$values['precision']}) " . strtoupper($values['null']) . " DEFAULT " . (int)$values['default'];
* }
* @return array
*/ /**
public function enumValuesForField($tableName, $fieldName) { * Return a bigint type-formatted string
$tablefield = "$tableName.$fieldName"; *
* @param array $values Contains a tokenised list of info about this data type
// Check already cached values for this field * @return string
if(!empty($this->enum_map[$tablefield])) { */
return explode(',', $this->enum_map[$tablefield]); public function bigint($values)
} {
return $this->int($values);
// Retrieve and cache these details from the database }
$classnameinfo = $this->preparedQuery(
"SELECT EnumList FROM SQLiteEnums WHERE TableColumn = ?", /**
array($tablefield) * Return a datetime type-formatted string
)->first(); * For SQLite3, we simply return the word 'TEXT', no other parameters are necessary
if($classnameinfo) { *
$valueList = $classnameinfo['EnumList']; * @param array $values Contains a tokenised list of info about this data type
$this->enum_map[$tablefield] = $valueList; * @return string
return explode(',', $valueList); */
} public function datetime($values)
{
// Fallback to empty list return "DATETIME";
return array(); }
}
/**
function dbDataType($type){ * Return a text type-formatted string
$values = array( *
'unsigned integer' => 'INT' * @param array $values Contains a tokenised list of info about this data type
); * @return string
*/
if(isset($values[$type])) return $values[$type]; public function text($values)
else return ''; {
} 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,207 +1,238 @@
<?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 * Create a connection of the appropriate type
* *
* @param array $databaseConfig * @skipUpgrade
* @param string $error Error message passed by value * @param array $databaseConfig
* @return mixed|null Either the connection object, or null if error * @param string $error Error message passed by value
*/ * @return mixed|null Either the connection object, or null if error
protected function createConnection($databaseConfig, &$error) { */
$error = null; protected function createConnection($databaseConfig, &$error)
try { {
if(!file_exists($databaseConfig['path'])) { $error = null;
self::create_db_dir($databaseConfig['path']); try {
self::secure_db_dir($databaseConfig['path']); if (!file_exists($databaseConfig['path'])) {
} self::create_db_dir($databaseConfig['path']);
$file = $databaseConfig['path'] . '/' . $databaseConfig['database']; self::secure_db_dir($databaseConfig['path']);
$conn = null; }
$file = $databaseConfig['path'] . '/' . $databaseConfig['database'];
$conn = null;
switch($databaseConfig['type']) { switch ($databaseConfig['type']) {
case 'SQLite3Database': case 'SQLite3Database':
if(empty($databaseConfig['key'])) { if (empty($databaseConfig['key'])) {
$conn = @new SQLite3($file, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE); $conn = @new SQLite3($file, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE);
} else { } else {
$conn = @new SQLite3($file, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, $databaseConfig['key']); $conn = @new SQLite3(
} $file,
break; SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE,
case 'SQLite3PDODatabase': $databaseConfig['key']
// May throw a PDOException if fails );
$conn = @new PDO("sqlite:$file"); }
break; break;
default: case 'SQLite3PDODatabase':
$error = 'Invalid connection type'; // May throw a PDOException if fails
return null; $conn = @new PDO("sqlite:$file");
} break;
default:
$error = 'Invalid connection type: ' . $databaseConfig['type'];
return null;
}
if($conn) { if ($conn) {
return $conn; return $conn;
} else { } else {
$error = 'Unknown connection error'; $error = 'Unknown connection error';
return null; return null;
} }
} catch(Exception $ex) { } catch (Exception $ex) {
$error = $ex->getMessage(); $error = $ex->getMessage();
return null; return null;
} }
} }
public function requireDatabaseFunctions($databaseConfig) { public function requireDatabaseFunctions($databaseConfig)
$data = DatabaseAdapterRegistry::get_adapter($databaseConfig['type']); {
return !empty($data['supported']); $data = DatabaseAdapterRegistry::get_adapter($databaseConfig['type']);
} return !empty($data['supported']);
}
public function requireDatabaseServer($databaseConfig) { public function requireDatabaseServer($databaseConfig)
$path = $databaseConfig['path']; {
$error = ''; $path = $databaseConfig['path'];
$success = false; $error = '';
$success = false;
if(!$path) { if (!$path) {
$error = 'No database path provided'; $error = 'No database path provided';
} elseif(is_writable($path) || (!file_exists($path) && is_writable(dirname($path)))) { } elseif (is_writable($path) || (!file_exists($path) && is_writable(dirname($path)))) {
// check if folder is writeable // check if folder is writeable
$success = true; $success = true;
} else { } else {
$error = "Permission denied"; $error = "Permission denied";
} }
return array( return array(
'success' => $success, 'success' => $success,
'error' => $error, 'error' => $error,
'path' => $path 'path' => $path
); );
} }
/** /**
* Ensure a database connection is possible using credentials provided. * Ensure a database connection is possible using credentials provided.
* *
* @todo Validate path * @todo Validate path
* *
* @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)
// Do additional validation around file paths {
if(empty($databaseConfig['path'])) return array( // Do additional validation around file paths
'success' => false, if (empty($databaseConfig['path'])) {
'error' => "Missing directory path" return array(
); 'success' => false,
if(empty($databaseConfig['database'])) return array( 'error' => "Missing directory path"
'success' => false, );
'error' => "Missing database filename" }
); if (empty($databaseConfig['database'])) {
return array(
'success' => false,
'error' => "Missing database filename"
);
}
// Create and secure db directory // Create and secure db directory
$path = $databaseConfig['path']; $path = $databaseConfig['path'];
$dirCreated = self::create_db_dir($path); $dirCreated = self::create_db_dir($path);
if(!$dirCreated) return array( if (!$dirCreated) {
'success' => false, return array(
'error' => sprintf('Cannot create path: "%s"', $path) 'success' => false,
); 'error' => sprintf('Cannot create path: "%s"', $path)
$dirSecured = self::secure_db_dir($path); );
if(!$dirSecured) return array( }
'success' => false, $dirSecured = self::secure_db_dir($path);
'error' => sprintf('Cannot secure path through .htaccess: "%s"', $path) if (!$dirSecured) {
); return array(
'success' => false,
'error' => sprintf('Cannot secure path through .htaccess: "%s"', $path)
);
}
$conn = $this->createConnection($databaseConfig, $error); $conn = $this->createConnection($databaseConfig, $error);
$success = !empty($conn); $success = !empty($conn);
return array( return array(
'success' => $success, 'success' => $success,
'connection' => $conn, 'connection' => $conn,
'error' => $error 'error' => $error
); );
} }
public function getDatabaseVersion($databaseConfig) { public function getDatabaseVersion($databaseConfig)
$version = 0; {
$version = 0;
switch($databaseConfig['type']) { /** @skipUpgrade */
case 'SQLite3Database': switch ($databaseConfig['type']) {
$info = SQLite3::version(); case 'SQLite3Database':
$version = trim($info['versionString']); $info = SQLite3::version();
break; $version = trim($info['versionString']);
case 'SQLite3PDODatabase': break;
// Fallback to using sqlite_version() query case 'SQLite3PDODatabase':
$conn = $this->createConnection($databaseConfig, $error); // Fallback to using sqlite_version() query
if($conn) { $conn = $this->createConnection($databaseConfig, $error);
$version = $conn->getAttribute(PDO::ATTR_SERVER_VERSION); if ($conn) {
} $version = $conn->getAttribute(PDO::ATTR_SERVER_VERSION);
break; }
} break;
}
return $version; return $version;
} }
public function requireDatabaseVersion($databaseConfig) { public function requireDatabaseVersion($databaseConfig)
$success = false; {
$error = ''; $success = false;
$version = $this->getDatabaseVersion($databaseConfig); $error = '';
$version = $this->getDatabaseVersion($databaseConfig);
if($version) { if ($version) {
$success = version_compare($version, '3.3', '>='); $success = version_compare($version, '3.3', '>=');
if(!$success) { if (!$success) {
$error = "Your SQLite3 library version is $version. It's recommended you use at least 3.3."; $error = "Your SQLite3 library version is $version. It's recommended you use at least 3.3.";
} }
} }
return array( return array(
'success' => $success, 'success' => $success,
'error' => $error 'error' => $error
); );
} }
public function requireDatabaseOrCreatePermissions($databaseConfig) { public function requireDatabaseOrCreatePermissions($databaseConfig)
$conn = $this->createConnection($databaseConfig, $error); {
$success = $alreadyExists = !empty($conn); $conn = $this->createConnection($databaseConfig, $error);
return array( $success = $alreadyExists = !empty($conn);
'success' => $success, return array(
'alreadyExists' => $alreadyExists, 'success' => $success,
); 'alreadyExists' => $alreadyExists,
} );
}
/** /**
* Creates the provided directory and prepares it for * Creates the provided directory and prepares it for
* storing SQLlite. Use {@link secure_db_dir()} to * storing SQLlite. Use {@link secure_db_dir()} to
* secure it against unauthorized access. * secure it against unauthorized access.
* *
* @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); {
} return file_exists($path) || mkdir($path);
}
/** /**
* Secure the provided directory via web-access * Secure the provided directory via web-access
* by placing a .htaccess file in it. * by placing a .htaccess file in it.
* This is just required if the database directory * This is just required if the database directory
* is placed within a publically accessible webroot (the * is placed within a publically accessible webroot (the
* default path is in a hidden folder within assets/). * default path is in a hidden folder within assets/).
* *
* @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) { public function requireDatabaseAlterPermissions($databaseConfig)
// no concept of table-specific permissions; If you can connect you can alter schema {
return array( // no concept of table-specific permissions; If you can connect you can alter schema
'success' => true, return array(
'applies' => false 'success' => true,
); 'applies' => false
} );
}
} }

View File

@ -1,7 +1,7 @@
{ {
"name": "silverstripe/sqlite3", "name": "silverstripe/sqlite3",
"description": "Adds SQLite3 support to SilverStripe", "description": "Adds SQLite3 support to SilverStripe",
"type": "silverstripe-module", "type": "silverstripe-vendormodule",
"keywords": ["silverstripe", "sqlite3", "database"], "keywords": ["silverstripe", "sqlite3", "database"],
"authors": [ "authors": [
{ {
@ -14,11 +14,21 @@
} }
], ],
"require": { "require": {
"silverstripe/framework": "^3.2.0" "silverstripe/framework": "~4.0",
"silverstripe/vendor-plugin": "^1.0"
}, },
"extra": { "require-dev": {
"branch-alias": { "squizlabs/php_codesniffer": "^3"
"dev-master": "1.4.x-dev" },
"autoload": {
"psr-4": {
"SilverStripe\\SQLite\\": "code/"
} }
} },
"scripts": {
"lint": "phpcs code/ *.php",
"lint-clean": "phpcbf code/ *.php"
},
"minimum-stability": "dev",
"prefer-stable": true
} }

12
phpcs.xml.dist Normal file
View File

@ -0,0 +1,12 @@
<?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>