diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml
index e3f1373d8c6..8513a0f7cd0 100644
--- a/.github/workflows/continuous-integration.yml
+++ b/.github/workflows/continuous-integration.yml
@@ -44,13 +44,21 @@ jobs:
- "8.2"
dependencies:
- "highest"
+ extension:
+ - "pdo_sqlite"
include:
- os: "ubuntu-20.04"
php-version: "7.4"
dependencies: "lowest"
+ extension: "pdo_sqlite"
+ - os: "ubuntu-22.04"
+ php-version: "7.4"
+ dependencies: "highest"
+ extension: "sqlite3"
- os: "ubuntu-22.04"
php-version: "8.1"
dependencies: "highest"
+ extension: "sqlite3"
steps:
- name: "Checkout"
@@ -76,12 +84,12 @@ jobs:
php -r 'printf("Testing with libsqlite version %s\n", (new PDO("sqlite::memory:"))->query("select sqlite_version()")->fetch()[0]);'
- name: "Run PHPUnit"
- run: "vendor/bin/phpunit -c ci/github/phpunit/sqlite.xml --coverage-clover=coverage.xml"
+ run: "vendor/bin/phpunit -c ci/github/phpunit/${{ matrix.extension }}.xml --coverage-clover=coverage.xml"
- name: "Upload coverage file"
uses: "actions/upload-artifact@v3"
with:
- name: "phpunit-sqlite-${{ matrix.deps }}-${{ matrix.php-version }}.coverage"
+ name: "phpunit-${{ matrix.extension }}-${{ matrix.deps }}-${{ matrix.php-version }}.coverage"
path: "coverage.xml"
phpunit-oci8:
@@ -551,7 +559,7 @@ jobs:
path: "coverage.xml"
development-deps:
- name: "PHPUnit with SQLite and development dependencies"
+ name: "PHPUnit with PDO_SQLite and development dependencies"
runs-on: "ubuntu-22.04"
strategy:
@@ -577,7 +585,7 @@ jobs:
composer-options: "--prefer-dist"
- name: "Run PHPUnit"
- run: "vendor/bin/phpunit -c ci/github/phpunit/sqlite.xml"
+ run: "vendor/bin/phpunit -c ci/github/phpunit/pdo_sqlite.xml"
upload_coverage:
name: "Upload coverage to Codecov"
diff --git a/ci/github/phpunit/pdo_sqlite.xml b/ci/github/phpunit/pdo_sqlite.xml
new file mode 100644
index 00000000000..2ff89cc6f10
--- /dev/null
+++ b/ci/github/phpunit/pdo_sqlite.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ ../../../tests
+
+
+
+
+
+ ../../../src
+
+
+
diff --git a/ci/github/phpunit/sqlite.xml b/ci/github/phpunit/sqlite3.xml
similarity index 80%
rename from ci/github/phpunit/sqlite.xml
rename to ci/github/phpunit/sqlite3.xml
index 5b3b7f613c7..3c58120444c 100644
--- a/ci/github/phpunit/sqlite.xml
+++ b/ci/github/phpunit/sqlite3.xml
@@ -10,6 +10,10 @@
>
+
+
+
+
diff --git a/docs/en/reference/configuration.rst b/docs/en/reference/configuration.rst
index e8ac39b8af3..4086b13df32 100644
--- a/docs/en/reference/configuration.rst
+++ b/docs/en/reference/configuration.rst
@@ -113,6 +113,7 @@ interfaces to use. It can be configured in one of three ways:
- ``mysqli``: A MySQL driver that uses the mysqli extension.
- ``pdo_sqlite``: An SQLite driver that uses the pdo_sqlite PDO
extension.
+ - ``sqlite3``: An SQLite driver that uses the sqlite3 extension.
- ``pdo_pgsql``: A PostgreSQL driver that uses the pdo_pgsql PDO
extension.
- ``pdo_oci``: An Oracle driver that uses the pdo_oci PDO
@@ -155,6 +156,15 @@ pdo_sqlite
in-memory (non-persistent). Mutually exclusive with ``path``.
``path`` takes precedence.
+sqlite3
+^^^^^^^
+
+- ``path`` (string): The filesystem path to the database file.
+ Mutually exclusive with ``memory``. ``path`` takes precedence.
+- ``memory`` (boolean): True if the SQLite database should be
+ in-memory (non-persistent). Mutually exclusive with ``path``.
+ ``path`` takes precedence.
+
pdo_mysql
^^^^^^^^^
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
index 23e215e2b46..5771c4252a7 100644
--- a/phpcs.xml.dist
+++ b/phpcs.xml.dist
@@ -56,6 +56,12 @@
*/src/*
+
+
+
+ src/Driver/SQLite3/*
+
+
*/tests/*
diff --git a/src/Driver/API/SQLite/UserDefinedFunctions.php b/src/Driver/API/SQLite/UserDefinedFunctions.php
index 29e73d1f271..0da61280d47 100644
--- a/src/Driver/API/SQLite/UserDefinedFunctions.php
+++ b/src/Driver/API/SQLite/UserDefinedFunctions.php
@@ -2,6 +2,10 @@
namespace Doctrine\DBAL\Driver\API\SQLite;
+use Closure;
+use Doctrine\DBAL\Platforms\SqlitePlatform;
+
+use function array_merge;
use function strpos;
/**
@@ -11,6 +15,25 @@
*/
final class UserDefinedFunctions
{
+ private const DEFAULT_FUNCTIONS = [
+ 'sqrt' => ['callback' => [SqlitePlatform::class, 'udfSqrt'], 'numArgs' => 1],
+ 'mod' => ['callback' => [SqlitePlatform::class, 'udfMod'], 'numArgs' => 2],
+ 'locate' => ['callback' => [SqlitePlatform::class, 'udfLocate'], 'numArgs' => -1],
+ ];
+
+ /**
+ * @param Closure(string, callable, int): bool $callback
+ * @param array $additionalFunctions
+ */
+ public static function register(Closure $callback, array $additionalFunctions): void
+ {
+ $userDefinedFunctions = array_merge(self::DEFAULT_FUNCTIONS, $additionalFunctions);
+
+ foreach ($userDefinedFunctions as $function => $data) {
+ $callback($function, $data['callback'], $data['numArgs']);
+ }
+ }
+
/**
* User-defined function that implements MOD().
*
diff --git a/src/Driver/PDO/SQLite/Driver.php b/src/Driver/PDO/SQLite/Driver.php
index 9d56526c519..8c3bd7db86d 100644
--- a/src/Driver/PDO/SQLite/Driver.php
+++ b/src/Driver/PDO/SQLite/Driver.php
@@ -2,24 +2,16 @@
namespace Doctrine\DBAL\Driver\PDO\SQLite;
+use Closure;
use Doctrine\DBAL\Driver\AbstractSQLiteDriver;
+use Doctrine\DBAL\Driver\API\SQLite\UserDefinedFunctions;
use Doctrine\DBAL\Driver\PDO\Connection;
use Doctrine\DBAL\Driver\PDO\Exception;
-use Doctrine\DBAL\Platforms\SqlitePlatform;
use PDO;
use PDOException;
-use function array_merge;
-
final class Driver extends AbstractSQLiteDriver
{
- /** @var mixed[] */
- private array $userDefinedFunctions = [
- 'sqrt' => ['callback' => [SqlitePlatform::class, 'udfSqrt'], 'numArgs' => 1],
- 'mod' => ['callback' => [SqlitePlatform::class, 'udfMod'], 'numArgs' => 2],
- 'locate' => ['callback' => [SqlitePlatform::class, 'udfLocate'], 'numArgs' => -1],
- ];
-
/**
* {@inheritdoc}
*
@@ -27,15 +19,9 @@ final class Driver extends AbstractSQLiteDriver
*/
public function connect(array $params)
{
- $driverOptions = $params['driverOptions'] ?? [];
-
- if (isset($driverOptions['userDefinedFunctions'])) {
- $this->userDefinedFunctions = array_merge(
- $this->userDefinedFunctions,
- $driverOptions['userDefinedFunctions'],
- );
- unset($driverOptions['userDefinedFunctions']);
- }
+ $driverOptions = $params['driverOptions'] ?? [];
+ $userDefinedFunctions = $driverOptions['userDefinedFunctions'] ?? [];
+ unset($driverOptions['userDefinedFunctions']);
try {
$pdo = new PDO(
@@ -48,9 +34,10 @@ public function connect(array $params)
throw Exception::new($exception);
}
- foreach ($this->userDefinedFunctions as $fn => $data) {
- $pdo->sqliteCreateFunction($fn, $data['callback'], $data['numArgs']);
- }
+ UserDefinedFunctions::register(
+ Closure::fromCallable([$pdo, 'sqliteCreateFunction']),
+ $userDefinedFunctions,
+ );
return new Connection($pdo);
}
diff --git a/src/Driver/SQLite3/Connection.php b/src/Driver/SQLite3/Connection.php
new file mode 100644
index 00000000000..de9a8e031ba
--- /dev/null
+++ b/src/Driver/SQLite3/Connection.php
@@ -0,0 +1,107 @@
+connection = $connection;
+ }
+
+ public function prepare(string $sql): Statement
+ {
+ try {
+ $statement = $this->connection->prepare($sql);
+ } catch (\Exception $e) {
+ throw Exception::new($e);
+ }
+
+ assert($statement !== false);
+
+ return new Statement($this->connection, $statement);
+ }
+
+ public function query(string $sql): Result
+ {
+ try {
+ $result = $this->connection->query($sql);
+ } catch (\Exception $e) {
+ throw Exception::new($e);
+ }
+
+ assert($result !== false);
+
+ return new Result($result, $this->connection->changes());
+ }
+
+ /** @inheritdoc */
+ public function quote($value, $type = ParameterType::STRING): string
+ {
+ return sprintf('\'%s\'', SQLite3::escapeString($value));
+ }
+
+ public function exec(string $sql): int
+ {
+ try {
+ $this->connection->exec($sql);
+ } catch (\Exception $e) {
+ throw Exception::new($e);
+ }
+
+ return $this->connection->changes();
+ }
+
+ /** @inheritdoc */
+ public function lastInsertId($name = null): int
+ {
+ return $this->connection->lastInsertRowID();
+ }
+
+ public function beginTransaction(): bool
+ {
+ try {
+ return $this->connection->exec('BEGIN');
+ } catch (\Exception $e) {
+ return false;
+ }
+ }
+
+ public function commit(): bool
+ {
+ try {
+ return $this->connection->exec('COMMIT');
+ } catch (\Exception $e) {
+ return false;
+ }
+ }
+
+ public function rollBack(): bool
+ {
+ try {
+ return $this->connection->exec('ROLLBACK');
+ } catch (\Exception $e) {
+ return false;
+ }
+ }
+
+ public function getNativeConnection(): SQLite3
+ {
+ return $this->connection;
+ }
+
+ public function getServerVersion(): string
+ {
+ return SQLite3::version()['versionString'];
+ }
+}
diff --git a/src/Driver/SQLite3/Driver.php b/src/Driver/SQLite3/Driver.php
new file mode 100644
index 00000000000..55186235bff
--- /dev/null
+++ b/src/Driver/SQLite3/Driver.php
@@ -0,0 +1,33 @@
+enableExceptions(true);
+
+ UserDefinedFunctions::register(
+ Closure::fromCallable([$connection, 'createFunction']),
+ $params['driverOptions']['userDefinedFunctions'] ?? [],
+ );
+
+ return new Connection($connection);
+ }
+}
diff --git a/src/Driver/SQLite3/Exception.php b/src/Driver/SQLite3/Exception.php
new file mode 100644
index 00000000000..3ca1190bc8c
--- /dev/null
+++ b/src/Driver/SQLite3/Exception.php
@@ -0,0 +1,18 @@
+getMessage(), null, (int) $exception->getCode(), $exception);
+ }
+}
diff --git a/src/Driver/SQLite3/Result.php b/src/Driver/SQLite3/Result.php
new file mode 100644
index 00000000000..a51bd4e8be4
--- /dev/null
+++ b/src/Driver/SQLite3/Result.php
@@ -0,0 +1,91 @@
+result = $result;
+ $this->changes = $changes;
+ }
+
+ /** @inheritdoc */
+ public function fetchNumeric()
+ {
+ if ($this->result === null) {
+ return false;
+ }
+
+ return $this->result->fetchArray(SQLITE3_NUM);
+ }
+
+ /** @inheritdoc */
+ public function fetchAssociative()
+ {
+ if ($this->result === null) {
+ return false;
+ }
+
+ return $this->result->fetchArray(SQLITE3_ASSOC);
+ }
+
+ /** @inheritdoc */
+ public function fetchOne()
+ {
+ return FetchUtils::fetchOne($this);
+ }
+
+ /** @inheritdoc */
+ public function fetchAllNumeric(): array
+ {
+ return FetchUtils::fetchAllNumeric($this);
+ }
+
+ /** @inheritdoc */
+ public function fetchAllAssociative(): array
+ {
+ return FetchUtils::fetchAllAssociative($this);
+ }
+
+ /** @inheritdoc */
+ public function fetchFirstColumn(): array
+ {
+ return FetchUtils::fetchFirstColumn($this);
+ }
+
+ public function rowCount(): int
+ {
+ return $this->changes;
+ }
+
+ public function columnCount(): int
+ {
+ if ($this->result === null) {
+ return 0;
+ }
+
+ return $this->result->numColumns();
+ }
+
+ public function free(): void
+ {
+ if ($this->result === null) {
+ return;
+ }
+
+ $this->result->finalize();
+ $this->result = null;
+ }
+}
diff --git a/src/Driver/SQLite3/Statement.php b/src/Driver/SQLite3/Statement.php
new file mode 100644
index 00000000000..878627a6a2c
--- /dev/null
+++ b/src/Driver/SQLite3/Statement.php
@@ -0,0 +1,119 @@
+ SQLITE3_NULL,
+ ParameterType::INTEGER => SQLITE3_INTEGER,
+ ParameterType::STRING => SQLITE3_TEXT,
+ ParameterType::ASCII => SQLITE3_TEXT,
+ ParameterType::BINARY => SQLITE3_BLOB,
+ ParameterType::LARGE_OBJECT => SQLITE3_BLOB,
+ ParameterType::BOOLEAN => SQLITE3_INTEGER,
+ ];
+
+ private SQLite3 $connection;
+ private SQLite3Stmt $statement;
+
+ /** @internal The statement can be only instantiated by its driver connection. */
+ public function __construct(SQLite3 $connection, SQLite3Stmt $statement)
+ {
+ $this->connection = $connection;
+ $this->statement = $statement;
+ }
+
+ /** @inheritdoc */
+ public function bindValue($param, $value, $type = ParameterType::STRING): bool
+ {
+ if (func_num_args() < 3) {
+ Deprecation::trigger(
+ 'doctrine/dbal',
+ 'https://github.com/doctrine/dbal/pull/5558',
+ 'Not passing $type to Statement::bindValue() is deprecated.'
+ . ' Pass the type corresponding to the parameter being bound.',
+ );
+ }
+
+ return $this->statement->bindValue($param, $value, $this->convertParamType($type));
+ }
+
+ /** @inheritdoc */
+ public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null): bool
+ {
+ Deprecation::trigger(
+ 'doctrine/dbal',
+ 'https://github.com/doctrine/dbal/pull/5563',
+ '%s is deprecated. Use bindValue() instead.',
+ __METHOD__,
+ );
+
+ if (func_num_args() < 3) {
+ Deprecation::trigger(
+ 'doctrine/dbal',
+ 'https://github.com/doctrine/dbal/pull/5558',
+ 'Not passing $type to Statement::bindParam() is deprecated.'
+ . ' Pass the type corresponding to the parameter being bound.',
+ );
+ }
+
+ return $this->statement->bindParam($param, $variable, $this->convertParamType($type));
+ }
+
+ /** @inheritdoc */
+ public function execute($params = null): Result
+ {
+ if ($params !== null) {
+ Deprecation::trigger(
+ 'doctrine/dbal',
+ 'https://github.com/doctrine/dbal/pull/5556',
+ 'Passing $params to Statement::execute() is deprecated. Bind parameters using'
+ . ' Statement::bindParam() or Statement::bindValue() instead.',
+ );
+
+ foreach ($params as $param => $value) {
+ if (is_int($param)) {
+ $this->bindValue($param + 1, $value, ParameterType::STRING);
+ } else {
+ $this->bindValue($param, $value, ParameterType::STRING);
+ }
+ }
+ }
+
+ try {
+ $result = $this->statement->execute();
+ } catch (\Exception $e) {
+ throw Exception::new($e);
+ }
+
+ assert($result !== false);
+
+ return new Result($result, $this->connection->changes());
+ }
+
+ private function convertParamType(int $type): int
+ {
+ if (! isset(self::PARAM_TYPE_MAP[$type])) {
+ throw UnknownParameterType::new($type);
+ }
+
+ return self::PARAM_TYPE_MAP[$type];
+ }
+}
diff --git a/src/DriverManager.php b/src/DriverManager.php
index a1a2a2185b8..5c30088ec28 100644
--- a/src/DriverManager.php
+++ b/src/DriverManager.php
@@ -7,6 +7,7 @@
use Doctrine\DBAL\Driver\Mysqli;
use Doctrine\DBAL\Driver\OCI8;
use Doctrine\DBAL\Driver\PDO;
+use Doctrine\DBAL\Driver\SQLite3;
use Doctrine\DBAL\Driver\SQLSrv;
use Doctrine\Deprecations\Deprecation;
@@ -88,6 +89,7 @@ final class DriverManager
'pdo_sqlsrv' => PDO\SQLSrv\Driver::class,
'mysqli' => Mysqli\Driver::class,
'sqlsrv' => SQLSrv\Driver::class,
+ 'sqlite3' => SQLite3\Driver::class,
];
/**
diff --git a/src/Types/DecimalType.php b/src/Types/DecimalType.php
index c70067f2b4f..144e97a0492 100644
--- a/src/Types/DecimalType.php
+++ b/src/Types/DecimalType.php
@@ -3,6 +3,7 @@
namespace Doctrine\DBAL\Types;
use Doctrine\DBAL\Platforms\AbstractPlatform;
+use Doctrine\DBAL\Platforms\SqlitePlatform;
use function is_float;
use function is_int;
@@ -37,7 +38,7 @@ public function convertToPHPValue($value, AbstractPlatform $platform)
{
// Some drivers starting from PHP 8.1 can represent decimals as float/int
// See also: https://github.com/doctrine/dbal/pull/4818
- if (PHP_VERSION_ID >= 80100 && (is_float($value) || is_int($value))) {
+ if ((PHP_VERSION_ID >= 80100 || $platform instanceof SqlitePlatform) && (is_float($value) || is_int($value))) {
return (string) $value;
}
diff --git a/tests/Functional/Driver/PDO/SQLite/DriverTest.php b/tests/Functional/Driver/PDO/SQLite/DriverTest.php
index 38f1395ba4c..294bb251d51 100644
--- a/tests/Functional/Driver/PDO/SQLite/DriverTest.php
+++ b/tests/Functional/Driver/PDO/SQLite/DriverTest.php
@@ -2,6 +2,7 @@
namespace Doctrine\DBAL\Tests\Functional\Driver\PDO\SQLite;
+use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver as DriverInterface;
use Doctrine\DBAL\Driver\PDO\SQLite\Driver;
use Doctrine\DBAL\Tests\Functional\Driver\AbstractDriverTest;
@@ -21,13 +22,30 @@ protected function setUp(): void
self::markTestSkipped('This test requires the pdo_sqlite driver.');
}
- public function testReturnsDatabaseNameWithoutDatabaseNameParameter(): void
+ protected static function getDatabaseNameForConnectionWithoutDatabaseNameParameter(): ?string
{
- self::markTestSkipped('SQLite does not support the concept of a database.');
+ return 'main';
}
protected function createDriver(): DriverInterface
{
return new Driver();
}
+
+ public function testRegisterCustomFunction(): void
+ {
+ $params = $this->connection->getParams();
+ $params['driverOptions']['userDefinedFunctions'] = [
+ 'my_add' => ['callback' => static fn (int $a, int $b): int => $a + $b, 'numArgs' => 2],
+ ];
+
+ $connection = new Connection(
+ $params,
+ $this->connection->getDriver(),
+ $this->connection->getConfiguration(),
+ $this->connection->getEventManager(),
+ );
+
+ self::assertSame(42, (int) $connection->fetchOne('SELECT my_add(20, 22)'));
+ }
}
diff --git a/tests/Functional/Driver/SQLite3/DriverTest.php b/tests/Functional/Driver/SQLite3/DriverTest.php
new file mode 100644
index 00000000000..97c2b53e5b0
--- /dev/null
+++ b/tests/Functional/Driver/SQLite3/DriverTest.php
@@ -0,0 +1,51 @@
+connection->getParams();
+ $params['driverOptions']['userDefinedFunctions'] = [
+ 'my_add' => ['callback' => static fn (int $a, int $b): int => $a + $b, 'numArgs' => 2],
+ ];
+
+ $connection = new Connection(
+ $params,
+ $this->connection->getDriver(),
+ $this->connection->getConfiguration(),
+ $this->connection->getEventManager(),
+ );
+
+ self::assertSame(42, $connection->fetchOne('SELECT my_add(20, 22)'));
+ }
+}
diff --git a/tests/Functional/StatementTest.php b/tests/Functional/StatementTest.php
index 8f3863f837b..668636e8f30 100644
--- a/tests/Functional/StatementTest.php
+++ b/tests/Functional/StatementTest.php
@@ -247,6 +247,10 @@ public function testBindInvalidNamedParameter(): void
self::markTestSkipped('The driver does not support named statement parameters');
}
+ if (TestUtil::isDriverOneOf('sqlite3')) {
+ self::markTestSkipped('SQLite3 does not report this error');
+ }
+
if (PHP_VERSION_ID < 80000 && TestUtil::isDriverOneOf('pdo_oci')) {
self::markTestSkipped('pdo_oci on PHP 7 does not report this error');
}
diff --git a/tests/TestUtil.php b/tests/TestUtil.php
index 74fede57b5c..c6dd4b33289 100644
--- a/tests/TestUtil.php
+++ b/tests/TestUtil.php
@@ -200,6 +200,7 @@ private static function mapConnectionParameters(array $configuration, string $pr
'password',
'host',
'dbname',
+ 'memory',
'port',
'server',
'ssl_key',