diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml new file mode 100644 index 0000000..25bc29b --- /dev/null +++ b/.github/workflows/continuous-integration.yml @@ -0,0 +1,31 @@ +name: "Continuous Integration" + +on: + pull_request: + push: + branches: + tags: + +jobs: + matrix: + name: Generate job matrix + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.matrix.outputs.matrix }} + steps: + - name: Gather CI configuration + id: matrix + uses: laminas/laminas-ci-matrix-action@v1 + + qa: + name: QA Checks + needs: [matrix] + runs-on: ${{ matrix.operatingSystem }} + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.matrix.outputs.matrix) }} + steps: + - name: ${{ matrix.name }} + uses: laminas/laminas-continuous-integration-action@v1 + with: + job: ${{ matrix.job }} \ No newline at end of file diff --git a/.laminas-ci.json b/.laminas-ci.json new file mode 100644 index 0000000..4e488ab --- /dev/null +++ b/.laminas-ci.json @@ -0,0 +1,16 @@ +{ + "ignore_php_platform_requirements": { + "8.4": true + }, + "exclude": [ + { + "name": "PHPUnit", + "php": "8.1" + }, + { + "name": "PHPUnit", + "php": "8.4", + "dependencies": "lowest" + } + ] +} \ No newline at end of file diff --git a/composer.json b/composer.json index 93d864f..a4c8cdb 100644 --- a/composer.json +++ b/composer.json @@ -25,13 +25,14 @@ } }, "extra": { - "php-db": { + "laminas": { "config-provider": "PhpDb\\Adapter\\Sqlite\\ConfigProvider" } }, "require": { "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", - "php-db/phpdb": "0.0.x-dev" + "php-db/phpdb": "dev-x.x.2", + "webmozart/assert": "^1.11" }, "require-dev": { "ext-pdo": "*", @@ -67,6 +68,8 @@ "test-coverage": "phpunit --colors=always --coverage-clover clover.xml", "test-integration": "phpunit --colors=always --testsuite \"integration test\"", "static-analysis": "psalm --shepherd --stats", + "sa-update-baseline": "psalm --update-baseline", + "sa-no-baseline": "psalm --shepherd --stats --ignore-baseline", "upload-coverage": "coveralls -v" } } diff --git a/composer.lock b/composer.lock index 39653d0..1100934 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f079faaaa3fddc12fdc8698b2c15dd48", + "content-hash": "684451c6d162458dea8a30023a84d89d", "packages": [ { "name": "brick/varexporter", @@ -259,16 +259,16 @@ }, { "name": "php-db/phpdb", - "version": "0.0.x-dev", + "version": "dev-x.x.2", "source": { "type": "git", "url": "https://github.com/php-db/phpdb.git", - "reference": "82b26147f3aab64c5f19402923a18acbb2991942" + "reference": "72985fad1331076ea8dee20890b7227e6460f1fc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-db/phpdb/zipball/82b26147f3aab64c5f19402923a18acbb2991942", - "reference": "82b26147f3aab64c5f19402923a18acbb2991942", + "url": "https://api.github.com/repos/php-db/phpdb/zipball/72985fad1331076ea8dee20890b7227e6460f1fc", + "reference": "72985fad1331076ea8dee20890b7227e6460f1fc", "shasum": "" }, "require": { @@ -322,7 +322,7 @@ "issues": "https://github.com/php-db/phpdb/issues", "source": "https://github.com/php-db/phpdb" }, - "time": "2025-07-07T04:06:26+00:00" + "time": "2025-07-14T06:08:40+00:00" }, { "name": "psr/container", @@ -376,6 +376,64 @@ "source": "https://github.com/php-fig/container/tree/2.0.2" }, "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": "^7.2 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.11.0" + }, + "time": "2022-06-03T18:03:27+00:00" } ], "packages-dev": [ @@ -2498,16 +2556,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "2.1.0", + "version": "2.2.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68" + "reference": "b9e61a61e39e02dd90944e9115241c7f7e76bfd8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", - "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/b9e61a61e39e02dd90944e9115241c7f7e76bfd8", + "reference": "b9e61a61e39e02dd90944e9115241c7f7e76bfd8", "shasum": "" }, "require": { @@ -2539,9 +2597,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.1.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.2.0" }, - "time": "2025-02-19T13:28:12+00:00" + "time": "2025-07-13T07:04:09+00:00" }, { "name": "phpunit/php-code-coverage", @@ -2880,16 +2938,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.26", + "version": "11.5.27", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "4ad8fe263a0b55b54a8028c38a18e3c5bef312e0" + "reference": "446d43867314781df7e9adf79c3ec7464956fd8f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4ad8fe263a0b55b54a8028c38a18e3c5bef312e0", - "reference": "4ad8fe263a0b55b54a8028c38a18e3c5bef312e0", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/446d43867314781df7e9adf79c3ec7464956fd8f", + "reference": "446d43867314781df7e9adf79c3ec7464956fd8f", "shasum": "" }, "require": { @@ -2899,7 +2957,7 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.13.1", + "myclabs/deep-copy": "^1.13.3", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.2", @@ -2961,7 +3019,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.26" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.27" }, "funding": [ { @@ -2985,7 +3043,7 @@ "type": "tidelift" } ], - "time": "2025-07-04T05:58:21+00:00" + "time": "2025-07-11T04:10:06+00:00" }, { "name": "psalm/plugin-phpunit", @@ -5326,16 +5384,16 @@ }, { "name": "vimeo/psalm", - "version": "6.12.1", + "version": "6.13.0", "source": { "type": "git", "url": "https://github.com/vimeo/psalm.git", - "reference": "e71404b0465be25cf7f8a631b298c01c5ddd864f" + "reference": "70cdf647255a1362b426bb0f522a85817b8c791c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vimeo/psalm/zipball/e71404b0465be25cf7f8a631b298c01c5ddd864f", - "reference": "e71404b0465be25cf7f8a631b298c01c5ddd864f", + "url": "https://api.github.com/repos/vimeo/psalm/zipball/70cdf647255a1362b426bb0f522a85817b8c791c", + "reference": "70cdf647255a1362b426bb0f522a85817b8c791c", "shasum": "" }, "require": { @@ -5440,7 +5498,7 @@ "issues": "https://github.com/vimeo/psalm/issues", "source": "https://github.com/vimeo/psalm" }, - "time": "2025-07-04T09:56:28+00:00" + "time": "2025-07-14T09:59:17+00:00" }, { "name": "webimpress/coding-standard", @@ -5496,64 +5554,6 @@ } ], "time": "2024-10-16T06:55:17+00:00" - }, - { - "name": "webmozart/assert", - "version": "1.11.0", - "source": { - "type": "git", - "url": "https://github.com/webmozarts/assert.git", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", - "shasum": "" - }, - "require": { - "ext-ctype": "*", - "php": "^7.2 || ^8.0" - }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<4.6.1 || 4.6.2" - }, - "require-dev": { - "phpunit/phpunit": "^8.5.13" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.10-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "support": { - "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.11.0" - }, - "time": "2022-06-03T18:03:27+00:00" } ], "aliases": [], diff --git a/phpunit.xml.dist b/phpunit.xml.dist index afc65b3..819d95b 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -14,18 +14,16 @@ failOnDeprecation="true" failOnWarning="true"> - - test/unit + + ./test/unit - - test/integration + + ./test/integration - - - + - src + ./src \ No newline at end of file diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 23267c4..6a39fe2 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,8 +1,570 @@ - - - + + + + + + + + + + + + + + + + + + + + connectionParameters]]> + + + resource->getAttribute(\PDO::ATTR_DRIVER_NAME)]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + data['sqlite_columns'][$schema]]]> + data['sqlite_columns'][$schema][$table]]]> + + + + data['columns'][$schema]]]> + data['columns'][$schema][$table]]]> + data['constraints'][$schema]]]> + data['constraints'][$schema][$table]]]> + data['sqlite_columns'][$schema]]]> + data['sqlite_columns'][$schema][$table]]]> + data['table_names'][$schema]]]> + data['triggers'][$schema]]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + toArray()]]> + + + + + + + quoteTrustedValue($value)]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + processInfo['paramPrefix']]]> + processInfo['paramPrefix']]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + getConfig()['db']['connection']]]> + + + + + + + + factory]]> + + + + + + + + factory]]> + factory]]> + factory]]> + factory]]> + + + + + + + + + + + + getMockBuilder(Driver::class) + ->setConstructorArgs([ + $this->mockConnection, + $this->mockStatement, + ]) + ->getMock()]]> + getMockBuilder(Statement::class)->getMock()]]> + + + + + + + + + + + + adapter]]> + + + + + + driver]]> + platform]]> + + + + + + adapter]]> + adapter]]> + adapter]]> + adapter]]> + adapter]]> + adapter]]> + adapter]]> + adapter]]> + adapter]]> + adapter]]> + adapter]]> + adapter]]> + adapter]]> + adapter]]> + adapter]]> + adapter]]> + adapter->DrivER]]> + adapter->PlatForm]]> + adapter->driver]]> + adapter->foo]]> + adapter->platform]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + [ + PlatformInterface::class => Platform\Sqlite::class, + ProfilerInterface::class => Profiler::class, + ], + 'factories' => [ + AdapterInterface::class => AdapterServiceFactory::class, + DriverInterface::class => Driver\Pdo\DriverFactory::class, + Platform\Sqlite::class => InvokableFactory::class, + Profiler::class => InvokableFactory::class, + ], + ]]]> + + + + + + + + + + pdo]]> + + + + + + pdo]]> + pdo]]> + pdo]]> + pdo]]> + pdo]]> + pdo]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + [ + PlatformInterface::class => Platform\Sqlite::class, + ProfilerInterface::class => Profiler::class, + ], + 'factories' => [ + AdapterInterface::class => AdapterServiceFactory::class, + DriverInterface::class => Driver\Pdo\DriverFactory::class, + Platform\Sqlite::class => InvokableFactory::class, + Profiler::class => InvokableFactory::class, + ], + ]]]> + + + + + + + + + assertEquals(E_USER_NOTICE, $errno); + $this->assertEquals( + $errstr, + 'Attempting to quote a value in PhpDb\Adapter\Sqlite\Platform\Sqlite without extension/driver support can ' + . 'introduce security vulnerabilities in a production environment' + ); + $raisedNotice = true; + }]]> + assertEquals(E_USER_NOTICE, $errno); + $this->assertEquals( + $errstr, + 'Attempting to quote a value in PhpDb\Adapter\Sqlite\Platform\Sqlite without extension/driver support can ' + . 'introduce security vulnerabilities in a production environment' + ); + $raisedNotice = true; + }]]> + + + + + + + + + + + + + + + + + diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index 04cb9f2..ce40636 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -4,19 +4,30 @@ namespace PhpDb\Adapter\Sqlite; +use Laminas\ServiceManager\Factory\InvokableFactory; use PhpDb\Adapter\AdapterInterface; +use PhpDb\Adapter\Driver\ConnectionInterface; +use PhpDb\Adapter\Driver\PdoConnectionInterface; use PhpDb\Adapter\Driver\DriverInterface; +use PhpDb\Adapter\Driver\PdoDriverInterface; +use PhpDb\Adapter\Driver\ResultInterface; +use PhpDb\Adapter\Driver\StatementInterface; +use PhpDb\Adapter\Driver\Pdo\Result; +use PhpDb\Adapter\Driver\Pdo\Statement; use PhpDb\Adapter\Platform\PlatformInterface; use PhpDb\Adapter\Profiler\Profiler; use PhpDb\Adapter\Profiler\ProfilerInterface; -use Laminas\ServiceManager\Factory\InvokableFactory; +use PhpDb\Container\AdapterManager; +use PhpDb\Metadata\MetadataInterface; +use PhpDb\ResultSet; -readonly class ConfigProvider +final class ConfigProvider { public function __invoke(): array { return [ - 'dependencies' => $this->getDependencies(), + 'dependencies' => $this->getDependencies(), + AdapterManager::class => $this->getAdapterManagerConfig(), ]; } @@ -24,14 +35,49 @@ public function getDependencies(): array { return [ 'aliases' => [ - PlatformInterface::class => Platform\Sqlite::class, - ProfilerInterface::class => Profiler::class, + MetadataInterface::class => Metadata\Source\SqliteMetadata::class, + ], + 'factories' => [ + Metadata\Source\SqliteMetadata::class => Container\MetadataInterfaceFactory::class, + ], + 'delegators' => [ + AdapterManager::class => [ + Container\AdapterManagerDelegator::class, + ], + ], + ]; + } + + public function getAdapterManagerConfig(): array + { + return [ + 'aliases' => [ + 'SQLite' => Driver\Pdo\Pdo::class, + 'Sqlite' => Driver\Pdo\Pdo::class, + 'sqlite' => Driver\Pdo\Pdo::class, + 'pdo' => Driver\Pdo\Pdo::class, + 'pdo_sqlite' => Driver\Pdo\Pdo::class, + 'pdosqlite' => Driver\Pdo\Pdo::class, + 'pdodriver' => Driver\Pdo\Pdo::class, + ConnectionInterface::class => Driver\Pdo\Connection::class, + PdoConnectionInterface::class => Driver\Pdo\Connection::class, + DriverInterface::class => Driver\Pdo\Pdo::class, + PdoDriverInterface::class => Driver\Pdo\Pdo::class, + PlatformInterface::class => Platform\Sqlite::class, + ProfilerInterface::class => Profiler::class, + ResultInterface::class => Result::class, + ResultSet\ResultSetInterface::class => ResultSet\ResultSet::class, + StatementInterface::class => Statement::class, ], 'factories' => [ - AdapterInterface::class => AdapterServiceFactory::class, - DriverInterface::class => Driver\Pdo\DriverFactory::class, - Platform\Sqlite::class => InvokableFactory::class, - Profiler::class => InvokableFactory::class, + AdapterInterface::class => Container\AdapterFactory::class, + Driver\Pdo\Connection::class => Container\PdoConnectionFactory::class, + Driver\Pdo\Pdo::class => Container\PdoDriverFactory::class, + Result::class => Container\PdoResultFactory::class, + Statement::class => Container\PdoStatementFactory::class, + Platform\Sqlite::class => Container\PlatformInterfaceFactory::class, + Profiler::class => InvokableFactory::class, + ResultSet\ResultSet::class => InvokableFactory::class, ], ]; } diff --git a/src/Container/AdapterManagerDelegator.php b/src/Container/AdapterManagerDelegator.php new file mode 100644 index 0000000..8e5fb7c --- /dev/null +++ b/src/Container/AdapterManagerDelegator.php @@ -0,0 +1,28 @@ +configure( + (new ConfigProvider())->getAdapterManagerConfig() + ); + + return $adapterManager; + } +} diff --git a/src/Container/MetadataInterfaceFactory.php b/src/Container/MetadataInterfaceFactory.php new file mode 100644 index 0000000..4d7194c --- /dev/null +++ b/src/Container/MetadataInterfaceFactory.php @@ -0,0 +1,24 @@ +get(AdapterInterface::class); + + return new SqliteMetadata( + $adapter + ); + } +} diff --git a/src/Container/PdoConnectionFactory.php b/src/Container/PdoConnectionFactory.php new file mode 100644 index 0000000..7499c1e --- /dev/null +++ b/src/Container/PdoConnectionFactory.php @@ -0,0 +1,26 @@ +get('config'); + + /** @var array $dbConfig */ + $dbConfig = $config['db'] ?? []; + + /** @var array $connectionConfig */ + $connectionConfig = $dbConfig['connection'] ?? []; + + return new Connection($connectionConfig); + } +} diff --git a/src/Container/PdoDriverFactory.php b/src/Container/PdoDriverFactory.php index 71fd400..9e06103 100644 --- a/src/Container/PdoDriverFactory.php +++ b/src/Container/PdoDriverFactory.php @@ -10,6 +10,7 @@ use PhpDb\Adapter\Driver\PdoDriverInterface; use PhpDb\Adapter\Driver\ResultInterface; use PhpDb\Adapter\Driver\StatementInterface; +use PhpDb\Adapter\Sqlite\Driver\Pdo\Feature\SqliteRowCounter; use PhpDb\Adapter\Sqlite\Driver\Pdo\Connection; use PhpDb\Adapter\Sqlite\Driver\Pdo\Pdo as PdoDriver; use PhpDb\Container\AdapterManager; @@ -34,7 +35,8 @@ public function __invoke(ContainerInterface $container): PdoDriverInterface&PdoD return new PdoDriver( $connectionInstance, $statementInstance, - $resultInstance + $resultInstance, + [new SqliteRowCounter()], ); } } diff --git a/src/Container/PdoResultFactory.php b/src/Container/PdoResultFactory.php new file mode 100644 index 0000000..109bba6 --- /dev/null +++ b/src/Container/PdoResultFactory.php @@ -0,0 +1,17 @@ +get('config'); + + /** @var array $dbConfig */ + $dbConfig = $config['db'] ?? []; + + /** @var array $options */ + $options = $dbConfig['options'] ?? []; + + return new Statement(options: $options); + } +} diff --git a/src/Container/PlatformFactory.php b/src/Container/PlatformFactory.php deleted file mode 100644 index feb8654..0000000 --- a/src/Container/PlatformFactory.php +++ /dev/null @@ -1,23 +0,0 @@ -get(AdapterManager::class); + + /** @var array $config */ + $config = $container->get('config'); + + /** @var array $dbConfig */ + $dbConfig = $config['db'] ?? []; + + /** @var string $driver */ + $driver = $dbConfig['driver']; + + /** @var PdoDriverInterface|PDO $driverInstance */ + $driverInstance = $adapterManager->get($driver); + + return new Sqlite($driverInstance); + } +} diff --git a/src/Driver/Pdo/Connection.php b/src/Driver/Pdo/Connection.php index 7f70f3a..d221b39 100644 --- a/src/Driver/Pdo/Connection.php +++ b/src/Driver/Pdo/Connection.php @@ -5,21 +5,21 @@ namespace PhpDb\Adapter\Sqlite\Driver\Pdo; use Override; -use PDO; use PDOException; -use PDOStatement; use PhpDb\Adapter\Driver\ConnectionInterface; use PhpDb\Adapter\Driver\Pdo\AbstractPdoConnection; use PhpDb\Adapter\Exception; +use Webmozart\Assert\Assert; use function array_diff_key; -use function implode; use function is_int; use function is_string; +use function str_starts_with; use function strtolower; class Connection extends AbstractPdoConnection { + public final const CURRENT_SCHEMA = 'main'; /** * {@inheritDoc} */ @@ -30,13 +30,7 @@ public function getCurrentSchema(): string|bool $this->connect(); } - /** @var PDOStatement $result */ - $result = $this->resource->query('PRAGMA database_list'); - if ($result instanceof PDOStatement) { - return $result->fetchColumn(); - } - - return false; + return self::CURRENT_SCHEMA; } /** @@ -44,6 +38,11 @@ public function getCurrentSchema(): string|bool * * @throws Exception\InvalidConnectionParametersException * @throws Exception\RuntimeException + * + * This connection class only supports the 'dsn', or 'path' parameters. + * If 'dsn' is not provided, it will attempt to construct one from 'path'. + * If neither is provided, an exception will be thrown. + * If 'driver_options' is provided, it will merge with existing options. */ #[Override] public function connect(): ConnectionInterface @@ -52,32 +51,13 @@ public function connect(): ConnectionInterface return $this; } - $dsn = $username = $password = $hostname = $database = null; + $dsn = null; $options = []; foreach ($this->connectionParameters as $key => $value) { switch (strtolower($key)) { case 'dsn': $dsn = $value; break; - case 'user': - case 'username': - $username = (string) $value; - break; - case 'pass': - case 'password': - $password = (string) $value; - break; - case 'host': - case 'hostname': - $hostname = (string) $value; - break; - case 'database': - case 'dbname': - $database = (string) $value; - break; - case 'unix_socket': - $unixSocket = (string) $value; - break; case 'driver_options': $value = (array) $value; $options = array_diff_key($options, $value) + $value; @@ -88,49 +68,35 @@ public function connect(): ConnectionInterface } } - if (isset($hostname) && isset($unixSocket)) { + if (! is_string($dsn)) { throw new Exception\InvalidConnectionParametersException( - 'Ambiguous connection parameters, both hostname and unix_socket parameters were set', - (int) $this->connectionParameters + 'A dsn was not provided', + $this->connectionParameters ); } - if (! isset($dsn)) { - $dsn = []; - if (isset($database)) { - $dsn[] = "dbname={$database}"; - } - if (isset($hostname)) { - $dsn[] = "host={$hostname}"; - } - if (isset($port)) { - $dsn[] = "port={$port}"; - } - if (isset($charset)) { - $dsn[] = "charset={$charset}"; - } - if (isset($unixSocket)) { - $dsn[] = "unix_socket={$unixSocket}"; - } - if (isset($version)) { - $dsn[] = "version={$version}"; - } - $dsn = 'sqlite:' . implode(';', $dsn); - } - - if (! is_string($dsn)) { - throw new Exception\InvalidConnectionParametersException( - 'A dsn was not provided or could not be constructed from your parameters', - $this->connectionParameters + if (! str_starts_with($dsn, 'sqlite:')) { + Assert::fileExists( + $dsn, + 'The provided DSN does not point to a valid file.' + ); + Assert::readable( + $dsn, + 'The provided DSN does not point to a readable file.' + ); + Assert::writable( + $dsn, + 'The provided DSN does not point to a writable file.' ); + $dsn = 'sqlite:' . $dsn; } $this->dsn = $dsn; try { - $this->resource = new PDO($dsn, $username, $password, $options); - $this->resource->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - $this->driverName = strtolower($this->resource->getAttribute(PDO::ATTR_DRIVER_NAME)); + $this->resource = new \PDO(dsn: $dsn, options: $options); + $this->resource->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + $this->driverName = strtolower($this->resource->getAttribute(\PDO::ATTR_DRIVER_NAME)); } catch (PDOException $e) { $code = $e->getCode(); if (! is_int($code)) { diff --git a/src/Driver/Pdo/Feature/SqliteRowCounter.php b/src/Driver/Pdo/Feature/SqliteRowCounter.php index b3c03a4..014b66b 100644 --- a/src/Driver/Pdo/Feature/SqliteRowCounter.php +++ b/src/Driver/Pdo/Feature/SqliteRowCounter.php @@ -1,5 +1,7 @@ resultPrototype; /** @var Feature\SqliteRowCounter $sqliteRowCounter */ - $sqliteRowCounter = $this->getFeature('SqliteRowCounter'); + $sqliteRowCounter = $this->getFeature(Feature\SqliteRowCounter::class); $rowCount = null; if ($sqliteRowCounter && $resource->columnCount() > 0) { diff --git a/src/Metadata/Source/SqliteMetadata.php b/src/Metadata/Source/SqliteMetadata.php new file mode 100644 index 0000000..0d09a4e --- /dev/null +++ b/src/Metadata/Source/SqliteMetadata.php @@ -0,0 +1,400 @@ +data['schemas'])) { + return; + } + $this->prepareDataHierarchy('schemas'); + + $results = $this->fetchPragma('database_list'); + foreach ($results as $row) { + $schemas[] = $row['name']; + } + $this->data['schemas'] = $schemas; + } + + #[Override] + protected function loadTableNameData(string $schema): void + { + if (isset($this->data['table_names'][$schema])) { + return; + } + $this->prepareDataHierarchy('table_names', $schema); + + // FEATURE: Filename? + + $p = $this->adapter->getPlatform(); + + $sql = 'SELECT "name", "type", "sql" FROM ' . $p->quoteIdentifierChain([$schema, 'sqlite_master']) + . ' WHERE "type" IN (\'table\',\'view\') AND "name" NOT LIKE \'sqlite_%\''; + + $results = $this->adapter->query($sql, AdapterInterface::QUERY_MODE_EXECUTE); + $tables = []; + foreach ($results->toArray() as $row) { + if ('table' === $row['type']) { + $table = [ + 'table_type' => 'BASE TABLE', + 'view_definition' => null, // VIEW only + 'check_option' => null, // VIEW only + 'is_updatable' => null, // VIEW only + ]; + } else { + $table = [ + 'table_type' => 'VIEW', + 'view_definition' => null, + 'check_option' => 'NONE', + 'is_updatable' => false, + ]; + + // Parse out extra data + if (null !== ($data = $this->parseView($row['sql']))) { + $table = array_merge($table, $data); + } + } + $tables[$row['name']] = $table; + } + $this->data['table_names'][$schema] = $tables; + } + + #[Override] + protected function loadColumnData(string $table, string $schema): void + { + if (isset($this->data['columns'][$schema][$table])) { + return; + } + $this->prepareDataHierarchy('columns', $schema, $table); + $this->prepareDataHierarchy('sqlite_columns', $schema, $table); + + $results = $this->fetchPragma('table_info', $table, $schema); + + $columns = []; + + foreach ($results as $row) { + $columns[$row['name']] = [ + // cid appears to be zero-based, ordinal position needs to be one-based + 'ordinal_position' => $row['cid'] + 1, + 'column_default' => $row['dflt_value'], + 'is_nullable' => ! (bool) $row['notnull'], + 'data_type' => $row['type'], + 'character_maximum_length' => null, + 'character_octet_length' => null, + 'numeric_precision' => null, + 'numeric_scale' => null, + 'numeric_unsigned' => null, + 'erratas' => [], + ]; + // TODO: populate character_ and numeric_values with correct info + } + + $this->data['columns'][$schema][$table] = $columns; + $this->data['sqlite_columns'][$schema][$table] = $results; + } + + #[Override] + protected function loadConstraintData(string $table, string $schema): void + { + if (isset($this->data['constraints'][$schema][$table])) { + return; + } + + $this->prepareDataHierarchy('constraints', $schema, $table); + + $this->loadColumnData($table, $schema); + $primaryKey = []; + + foreach ($this->data['sqlite_columns'][$schema][$table] as $col) { + if ((bool) $col['pk']) { + $primaryKey[] = $col['name']; + } + } + + if (empty($primaryKey)) { + $primaryKey = null; + } + $constraints = []; + $indexes = $this->fetchPragma('index_list', $table, $schema); + foreach ($indexes as $index) { + if (! (bool) $index['unique']) { + continue; + } + $constraint = [ + 'constraint_name' => $index['name'], + 'constraint_type' => 'UNIQUE', + 'table_name' => $table, + 'columns' => [], + ]; + + $info = $this->fetchPragma('index_info', $index['name'], $schema); + + foreach ($info as $column) { + $constraint['columns'][] = $column['name']; + } + if ($primaryKey === $constraint['columns']) { + $constraint['constraint_type'] = 'PRIMARY KEY'; + $primaryKey = null; + } + $constraints[$constraint['constraint_name']] = $constraint; + } + + if (null !== $primaryKey) { + $constraintName = '_laminas_' . $table . '_PRIMARY'; + $constraints[$constraintName] = [ + 'constraint_name' => $constraintName, + 'constraint_type' => 'PRIMARY KEY', + 'table_name' => $table, + 'columns' => $primaryKey, + ]; + } + + $foreignKeys = $this->fetchPragma('foreign_key_list', $table, $schema); + + $id = $name = null; + foreach ($foreignKeys as $fk) { + if ($id !== $fk['id']) { + $id = $fk['id']; + // todo: decide on whether to continue to use _laminas_ + $name = '_laminas_' . $table . '_FOREIGN_KEY_' . ($id + 1); + $constraints[$name] = [ + 'constraint_name' => $name, + 'constraint_type' => 'FOREIGN KEY', + 'table_name' => $table, + 'columns' => [], + 'referenced_table_schema' => $schema, + 'referenced_table_name' => $fk['table'], + 'referenced_columns' => [], + // TODO: Verify match, on_update, and on_delete values conform to SQL Standard + 'match_option' => strtoupper($fk['match']), + 'update_rule' => strtoupper($fk['on_update']), + 'delete_rule' => strtoupper($fk['on_delete']), + ]; + } + $constraints[$name]['columns'][] = $fk['from']; + $constraints[$name]['referenced_columns'][] = $fk['to']; + } + + $this->data['constraints'][$schema][$table] = $constraints; + } + + #[Override] + protected function loadTriggerData(string $schema): void + { + if (isset($this->data['triggers'][$schema])) { + return; + } + + $this->prepareDataHierarchy('triggers', $schema); + + $p = $this->adapter->getPlatform(); + + $sql = 'SELECT "name", "tbl_name", "sql" FROM ' + . $p->quoteIdentifierChain([$schema, 'sqlite_master']) + . ' WHERE "type" = \'trigger\''; + + $results = $this->adapter->query($sql, AdapterInterface::QUERY_MODE_EXECUTE); + $triggers = []; + foreach ($results->toArray() as $row) { + $trigger = [ + 'trigger_name' => $row['name'], + 'event_manipulation' => null, // in $row['sql'] + 'event_object_catalog' => null, + 'event_object_schema' => $schema, + 'event_object_table' => $row['tbl_name'], + 'action_order' => 0, + 'action_condition' => null, // in $row['sql'] + 'action_statement' => null, // in $row['sql'] + 'action_orientation' => 'ROW', + 'action_timing' => null, // in $row['sql'] + 'action_reference_old_table' => null, + 'action_reference_new_table' => null, + 'action_reference_old_row' => 'OLD', + 'action_reference_new_row' => 'NEW', + 'created' => null, + ]; + + // Parse out extra data + if (null !== ($data = $this->parseTrigger($row['sql']))) { + $trigger = array_merge($trigger, $data); + } + $triggers[$trigger['trigger_name']] = $trigger; + } + + $this->data['triggers'][$schema] = $triggers; + } + + protected function fetchPragma(string $name, ?string $value = null, ?string $schema = null): array + { + $p = $this->adapter->getPlatform(); + + $sql = 'PRAGMA '; + + if (null !== $schema) { + $sql .= $p->quoteIdentifier($schema) . '.'; + } + $sql .= $name; + + if (null !== $value) { + $sql .= '(' . $p->quoteTrustedValue($value) . ')'; + } + + $results = $this->adapter->query($sql, AdapterInterface::QUERY_MODE_EXECUTE); + if ($results instanceof ResultSetInterface) { + return $results->toArray(); + } + return []; + } + + /** @return null|array */ + protected function parseView(string $sql): ?array + { + static $re = null; + if (null === $re) { + $identifierChain = $this->getIdentifierChainRegularExpression(); + $re = $this->buildRegularExpression([ + 'CREATE', + ['TEMP|TEMPORARY'], + 'VIEW', + ['IF', 'NOT', 'EXISTS'], + $identifierChain, + 'AS', + '(?.+)', + [';'], + ]); + } + + if (! preg_match($re, $sql, $matches)) { + return null; + } + + return [ + 'view_definition' => $matches['view_definition'], + ]; + } + + /** @return null|array */ + protected function parseTrigger(string $sql): ?array + { + static $re = null; + if (null === $re) { + $identifier = $this->getIdentifierRegularExpression(); + $identifierList = $this->getIdentifierListRegularExpression(); + $identifierChain = $this->getIdentifierChainRegularExpression(); + $re = $this->buildRegularExpression([ + 'CREATE', + ['TEMP|TEMPORARY'], + 'TRIGGER', + ['IF', 'NOT', 'EXISTS'], + $identifierChain, + ['(?BEFORE|AFTER|INSTEAD\\s+OF)'], + '(?DELETE|INSERT|UPDATE)', + ['OF', '(?' . $identifierList . ')'], + 'ON', + '(?' . $identifier . ')', + ['FOR', 'EACH', 'ROW'], + ['WHEN', '(?.+)'], + '(?BEGIN', + '.+', + 'END)', + [';'], + ]); + } + + if (! preg_match($re, $sql, $matches)) { + return null; + } + $data = []; + + foreach ($matches as $key => $value) { + if (is_string($key)) { + $data[$key] = $value; + } + } + + // Normalize data and populate defaults, if necessary + + $data['event_manipulation'] = strtoupper($data['event_manipulation']); + if (empty($data['action_condition'])) { + $data['action_condition'] = null; + } + if (! empty($data['action_timing'])) { + $data['action_timing'] = strtoupper($data['action_timing']); + if ('I' === $data['action_timing'][0]) { + // normalize the white-space between the two words + $data['action_timing'] = 'INSTEAD OF'; + } + } else { + $data['action_timing'] = 'AFTER'; + } + unset($data['column_usage']); + + return $data; + } + + protected function buildRegularExpression(array $re): string + { + foreach ($re as &$value) { + if (is_array($value)) { + $value = '(?:' . implode('\\s*+', $value) . '\\s*+)?'; + } else { + $value .= '\\s*+'; + } + } + unset($value); + $re = '/^' . implode('\\s*+', $re) . '$/'; + return $re; + } + + protected function getIdentifierRegularExpression(): string + { + static $re = null; + if (null === $re) { + $re = '(?:' . implode('|', [ + '"(?:[^"\\\\]++|\\\\.)*+"', + '`(?:[^`]++|``)*+`', + '\\[[^\\]]+\\]', + '[^\\s\\.]+', + ]) . ')'; + } + + return $re; + } + + protected function getIdentifierChainRegularExpression(): string + { + static $re = null; + if (null === $re) { + $identifier = $this->getIdentifierRegularExpression(); + $re = $identifier . '(?:\\s*\\.\\s*' . $identifier . ')*+'; + } + return $re; + } + + protected function getIdentifierListRegularExpression(): string + { + static $re = null; + if (null === $re) { + $identifier = $this->getIdentifierRegularExpression(); + $re = $identifier . '(?:\\s*,\\s*' . $identifier . ')*+'; + } + return $re; + } +} diff --git a/src/Platform/Sqlite.php b/src/Platform/Sqlite.php index ef7daa0..0071fb1 100644 --- a/src/Platform/Sqlite.php +++ b/src/Platform/Sqlite.php @@ -28,24 +28,8 @@ class Sqlite extends AbstractPlatform public function __construct( protected readonly PdoDriverInterface|\PDO $driver - ) { - if ( - ( - $this->driver instanceof \PDO - && $this->driver->getAttribute(\PDO::ATTR_DRIVER_NAME) === 'sqlite' - ) - || ( - $this->driver instanceof Pdo\Pdo - && $this->driver->getDatabasePlatformName() === 'Sqlite' - ) - ) { - $this->resource = $this->driver; - } - - throw new Exception\InvalidArgumentException( - '$driver must be a Sqlite PDO PhpDb\Adapter\Driver, Sqlite PDO instance' - ); - } + ) { + } /** * {@inheritDoc} diff --git a/test/integration/Container/AdapterFactoryTest.php b/test/integration/Container/AdapterFactoryTest.php new file mode 100644 index 0000000..272a092 --- /dev/null +++ b/test/integration/Container/AdapterFactoryTest.php @@ -0,0 +1,25 @@ +container); + self::assertInstanceOf(AdapterInterface::class, $adapter); + } +} diff --git a/test/integration/Container/PdoConnectionFactoryTest.php b/test/integration/Container/PdoConnectionFactoryTest.php new file mode 100644 index 0000000..6095dcd --- /dev/null +++ b/test/integration/Container/PdoConnectionFactoryTest.php @@ -0,0 +1,32 @@ +container); + self::assertInstanceOf(ConnectionInterface::class, $instance); + self::assertInstanceOf(PdoConnectionInterface::class, $instance); + self::assertInstanceOf(Connection::class, $instance); + } +} diff --git a/test/integration/Container/PdoDriverFactoryTest.php b/test/integration/Container/PdoDriverFactoryTest.php new file mode 100644 index 0000000..bc16fc5 --- /dev/null +++ b/test/integration/Container/PdoDriverFactoryTest.php @@ -0,0 +1,31 @@ +container); + + self::assertInstanceOf(PdoDriverInterface::class, $instance); + self::assertInstanceOf(Pdo::class, $instance); + } +} diff --git a/test/integration/Container/PdoResultFactoryTest.php b/test/integration/Container/PdoResultFactoryTest.php new file mode 100644 index 0000000..4b8d7ac --- /dev/null +++ b/test/integration/Container/PdoResultFactoryTest.php @@ -0,0 +1,31 @@ +container); + + self::assertInstanceOf(ResultInterface::class, $result); + self::assertInstanceOf(Result::class, $result); + } +} diff --git a/test/integration/Container/PdoStatementFactoryTest.php b/test/integration/Container/PdoStatementFactoryTest.php new file mode 100644 index 0000000..00b6bf2 --- /dev/null +++ b/test/integration/Container/PdoStatementFactoryTest.php @@ -0,0 +1,30 @@ +container); + self::assertInstanceOf(StatementInterface::class, $statement); + self::assertInstanceOf(Statement::class, $statement); + } +} diff --git a/test/integration/Container/PlatformInterfaceFactoryTest.php b/test/integration/Container/PlatformInterfaceFactoryTest.php new file mode 100644 index 0000000..fb3e29e --- /dev/null +++ b/test/integration/Container/PlatformInterfaceFactoryTest.php @@ -0,0 +1,30 @@ +container); + self::assertInstanceOf(PlatformInterface::class, $instance); + self::assertInstanceOf(Sqlite::class, $instance); + } +} diff --git a/test/integration/Container/SqliteMetadataFactoryTest.php b/test/integration/Container/SqliteMetadataFactoryTest.php new file mode 100644 index 0000000..ac0e550 --- /dev/null +++ b/test/integration/Container/SqliteMetadataFactoryTest.php @@ -0,0 +1,27 @@ +container); + self::assertInstanceOf(MetadataInterface::class, $metadata); + self::assertInstanceOf(SqliteMetadata::class, $metadata); + } +} diff --git a/test/integration/Container/TestAsset/SetupTrait.php b/test/integration/Container/TestAsset/SetupTrait.php new file mode 100644 index 0000000..1acd229 --- /dev/null +++ b/test/integration/Container/TestAsset/SetupTrait.php @@ -0,0 +1,105 @@ + []]; + + protected ?AdapterInterface $adapter; + + protected AdapterManager $adapterManager; + + protected ContainerInterface $container; + + protected DriverInterface|string|null $driver; + + #[Override] + protected function setUp(): void + { + $this->getAdapter(); + parent::setUp(); + } + + protected function getAdapter(array $config = []): AdapterInterface + { + $connectionConfig = [ + 'db' => [ + 'driver' => $this->driver ?? Pdo::class, + 'connection' => [ + 'dsn' => 'sqlite::memory:', + 'charset' => 'utf8', + 'driver_options' => [], + ], + 'options' => [ + //'buffer_results' => false, + ], + ], + ]; + + // merge service config from both PhpDb and PhpDb\Adapter\Mysql + $serviceManagerConfig = ArrayUtils::merge( + (new LaminasDbConfigProvider())()['dependencies'], + (new ConfigProvider())()['dependencies'] + ); + + $serviceManagerConfig = ArrayUtils::merge( + $serviceManagerConfig, + $connectionConfig + ); + + // prefer passed config over environment variables + if ($config !== []) { + $serviceManagerConfig = ArrayUtils::merge($serviceManagerConfig, $config); + } + + $serviceManagerConfig = ArrayUtils::merge( + $serviceManagerConfig, + [ + 'services' => [ + 'config' => $serviceManagerConfig, + ], + ] + ); + + $this->config = $serviceManagerConfig; + $this->container = new ServiceManager($this->config); + $this->adapterManager = $this->container->get(AdapterManager::class); + $this->adapter = $this->adapterManager->get(AdapterInterface::class); + + return $this->adapter; + } + + protected function getConfig(): array + { + return $this->config; + } + + protected function getHostname(): string + { + return $this->getConfig()['db']['connection']['hostname']; + } +} diff --git a/test/integration/Driver/Pdo/AbstractAdapterTestCase.php b/test/integration/Driver/Pdo/AbstractAdapterTestCase.php index ec2750e..66228b5 100644 --- a/test/integration/Driver/Pdo/AbstractAdapterTestCase.php +++ b/test/integration/Driver/Pdo/AbstractAdapterTestCase.php @@ -3,6 +3,7 @@ namespace PhpDbIntegrationTest\Adapter\Sqlite\Driver\Pdo; use PhpDb\Adapter\AdapterInterface; +use PhpDbIntegrationTest\Adapter\Sqlite\Container\TestAsset\SetupTrait; use PHPUnit\Framework\Attributes\CoversMethod; use PHPUnit\Framework\TestCase; @@ -12,7 +13,7 @@ #[CoversMethod(AdapterInterface::class, '__construct()')] abstract class AbstractAdapterTestCase extends TestCase { - use AdapterTrait; + use SetupTrait; public ?int $port = null; diff --git a/test/integration/Driver/Pdo/AdapterTrait.php b/test/integration/Driver/Pdo/AdapterTrait.php deleted file mode 100644 index 160b496..0000000 --- a/test/integration/Driver/Pdo/AdapterTrait.php +++ /dev/null @@ -1,25 +0,0 @@ -adapter === null) { - $this->fail('Adapter not initialized'); - } - - return $this->adapter; - } - - protected function getHostname(): ?string - { - return $this->hostname; - } -} diff --git a/test/integration/Driver/Pdo/ConnectionIntegrationTest.php b/test/integration/Driver/Pdo/ConnectionIntegrationTest.php index da42fdf..7acc4c3 100644 --- a/test/integration/Driver/Pdo/ConnectionIntegrationTest.php +++ b/test/integration/Driver/Pdo/ConnectionIntegrationTest.php @@ -4,9 +4,10 @@ namespace PhpDbIntegrationTest\Adapter\Sqlite\Driver\Pdo; -use PhpDb\Adapter\Sqlite\Driver\Pdo\Connection; -use PhpDbIntegrationTest\Adapter\Sqlite\Driver\Pdo\TestAsset\SqliteMemoryPdo; use PDO; +use PhpDb\Adapter\Driver\PdoConnectionInterface; +use PhpDb\Adapter\Sqlite\Driver\Pdo\Connection; +use PhpDbIntegrationTest\Adapter\Sqlite\Container\TestAsset\SetupTrait; use PHPUnit\Framework\Attributes\CoversMethod; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; @@ -27,61 +28,58 @@ #[Group('integration-pdo')] final class ConnectionIntegrationTest extends TestCase { - /** @var array */ - protected array $variables = ['driver' => 'pdo_sqlite', 'database' => ':memory:']; + use SetupTrait; public function testGetCurrentSchema(): void { - $connection = new Connection($this->variables); + $connection = $this->getAdapter()->getDriver()->getConnection(); self::assertIsString($connection->getCurrentSchema()); } - public function testSetResource(): void - { - $resource = new SqliteMemoryPdo(); - $connection = new Connection([]); - self::assertSame($connection, $connection->setResource($resource)); + // public function testSetResource(): void + // { + // $resource = $this->getAdapter()->getDriver()->getConnection()->getResource(); + // $connection = new Connection([]); + // self::assertSame($connection, $connection->setResource($resource)); - $connection->disconnect(); - unset($connection); - unset($resource); - } + // $connection->disconnect(); + // unset($connection); + // unset($resource); + // } public function testGetResource(): void { - $connection = new Connection($this->variables); + $connection = $this->getAdapter()->getDriver()->getConnection(); $connection->connect(); self::assertInstanceOf(PDO::class, $connection->getResource()); $connection->disconnect(); - unset($connection); } public function testConnect(): void { - $connection = new Connection($this->variables); + $connection = $this->getAdapter()->getDriver()->getConnection(); self::assertSame($connection, $connection->connect()); self::assertTrue($connection->isConnected()); $connection->disconnect(); - unset($connection); } public function testIsConnected(): void { - $connection = new Connection($this->variables); + $connection = $this->getAdapter()->getDriver()->getConnection(); self::assertFalse($connection->isConnected()); self::assertSame($connection, $connection->connect()); self::assertTrue($connection->isConnected()); $connection->disconnect(); - unset($connection); + //unset($connection); } public function testDisconnect(): void { - $connection = new Connection($this->variables); + $connection = $this->getAdapter()->getDriver()->getConnection(); $connection->connect(); self::assertTrue($connection->isConnected()); $connection->disconnect(); @@ -130,8 +128,11 @@ public function testGetLastGeneratedValue(): never public function testConnectReturnsConnectionWhenResourceSet(): void { - $resource = new SqliteMemoryPdo(); - $connection = new Connection([]); + /** @var PDO $resource */ + $resource = $this->getAdapter()->getDriver()->getConnection()->getResource(); + /** @var PdoConnectionInterface&Connection $connection */ + $connection = $this->getAdapter()->getDriver()->getConnection(); + self::assertInstanceOf(PdoConnectionInterface::class, $connection); $connection->setResource($resource); self::assertSame($connection, $connection->connect()); diff --git a/test/integration/Driver/Pdo/StatementTest.php b/test/integration/Driver/Pdo/StatementTest.php index df2bf9a..ab73c02 100644 --- a/test/integration/Driver/Pdo/StatementTest.php +++ b/test/integration/Driver/Pdo/StatementTest.php @@ -4,13 +4,12 @@ namespace PhpDbIntegrationTest\Adapter\Sqlite\Driver\Pdo; +use PDO; +use PDOStatement; use PhpDb\Adapter\Driver\StatementInterface; -use PhpDb\Adapter\Sqlite\Driver\Pdo\Connection; -use PhpDb\Adapter\Sqlite\Driver\Pdo\Driver; -use PhpDb\Adapter\Sqlite\Driver\Pdo\Result; -use PhpDb\Adapter\Sqlite\Driver\Pdo\Statement; -use PhpDbIntegrationTest\Adapter\Sqlite\Driver\Pdo\TestAsset\SqliteMemoryPdo; -use Override; +use PhpDb\Adapter\Driver\Pdo\Statement; +use PhpDb\Adapter\Driver\Pdo\Result; +use PhpDbIntegrationTest\Adapter\Sqlite\Container\TestAsset\SetupTrait; use PHPUnit\Framework\Attributes\CoversMethod; use PHPUnit\Framework\TestCase; @@ -25,27 +24,34 @@ #[CoversMethod(Statement::class, 'execute')] final class StatementTest extends TestCase { - protected Statement $statement; + use SetupTrait; public function testGetResource(): void { - $pdo = new SqliteMemoryPdo(); + /** @var PDO $pdo */ + $pdo = $this->getAdapter()->getDriver()->getConnection()->getResource(); + /** @var StatementInterface&Statement $statement */ + $statement = $this->getAdapter()->getDriver()->createStatement(); + /** @var PDOStatement $stmt */ $stmt = $pdo->prepare('SELECT 1'); - $this->statement->setResource($stmt); + $statement->setResource($stmt); - self::assertSame($stmt, $this->statement->getResource()); + self::assertSame($stmt, $statement->getResource()); } public function testSetSql(): void { - $this->statement->setSql('SELECT 1'); - self::assertEquals('SELECT 1', $this->statement->getSql()); + /** @var StatementInterface&Statement $statement */ + $statement = $this->getAdapter()->getDriver()->createStatement(); + $statement->setSql('SELECT 1'); + self::assertEquals('SELECT 1', $statement->getSql()); } public function testGetSql(): void { - $this->statement->setSql('SELECT 1'); - self::assertEquals('SELECT 1', $this->statement->getSql()); + $statement = $this->getAdapter()->getDriver()->createStatement(); + $statement->setSql('SELECT 1'); + self::assertEquals('SELECT 1', $statement->getSql()); } /** @@ -53,39 +59,27 @@ public function testGetSql(): void */ public function testPrepare(): void { - $this->statement->initialize(new SqliteMemoryPdo()); - self::assertInstanceOf(StatementInterface::class, $this->statement->prepare('SELECT 1')); + /** @var StatementInterface&Statement $statement */ + $statement = $this->getAdapter()->getDriver()->createStatement(); + self::assertInstanceOf(StatementInterface::class, $statement->prepare('SELECT 1')); } public function testIsPrepared(): void { - self::assertFalse($this->statement->isPrepared()); - $this->statement->initialize(new SqliteMemoryPdo()); - $this->statement->prepare('SELECT 1'); - self::assertTrue($this->statement->isPrepared()); + /** @var StatementInterface&Statement $statement */ + $statement = $this->getAdapter()->getDriver()->createStatement(); + self::assertFalse($statement->isPrepared()); + //$statement->initialize($resource); + $statement->prepare('SELECT 1'); + self::assertTrue($statement->isPrepared()); } public function testExecute(): void { - $this->statement->setDriver(new Driver(new Connection($pdo = new SqliteMemoryPdo()))); - $this->statement->initialize($pdo); - $this->statement->prepare('SELECT 1'); - self::assertInstanceOf(Result::class, $this->statement->execute()); + /** @var StatementInterface&Statement $statement */ + $statement = $this->getAdapter()->getDriver()->createStatement(); + //$statement->initialize($pdo); + $statement->prepare('SELECT 1'); + self::assertInstanceOf(Result::class, $statement->execute()); } - - /** - * Sets up the fixture, for example, opens a network connection. - * This method is called before a test is executed. - */ - #[Override] - protected function setUp(): void - { - $this->statement = new Statement(); - } - - /** - * Tears down the fixture, for example, closes a network connection. - * This method is called after a test is executed. - */ - protected function tearDown(): void {} } diff --git a/test/integration/Driver/Pdo/TestAsset/SqliteMemoryPdo.php b/test/integration/Driver/Pdo/TestAsset/SqliteMemoryPdo.php deleted file mode 100644 index 03e953c..0000000 --- a/test/integration/Driver/Pdo/TestAsset/SqliteMemoryPdo.php +++ /dev/null @@ -1,35 +0,0 @@ -exec($sql)) { - throw new Exception(sprintf( - "Error: %s, %s", - $this->errorCode() ?? '', - implode(",", $this->errorInfo()) - )); - } - } -} diff --git a/test/integration/Platform/SqliteTest.php b/test/integration/Platform/SqliteTest.php index c9e0986..c87ef12 100644 --- a/test/integration/Platform/SqliteTest.php +++ b/test/integration/Platform/SqliteTest.php @@ -1,55 +1,28 @@ */ - public array|PDO $adapters = []; + use SetupTrait; - /** - * @return void - */ - public function testQuoteValueWithPdoSqlite() + public function testQuoteValueWithPdoSqlite(): void { - if (! $this->adapters['pdo_sqlite'] instanceof PDO) { - $this->markTestSkipped('SQLite (PDO_SQLITE) not configured in unit test configuration file'); - } - - $sqlite = new Sqlite($this->adapters['pdo_sqlite']); - $value = $sqlite->quoteValue('value'); - self::assertEquals('\'value\'', $value); - - $sqlite = new Sqlite(new Driver(new Connection($this->adapters['pdo_sqlite']))); + $sqlite = $this->getAdapter()->getPlatform(); $value = $sqlite->quoteValue('value'); self::assertEquals('\'value\'', $value); } - - #[Override] - protected function setUp(): void - { - if (! extension_loaded('pdo')) { - $this->markTestSkipped(self::class . ' integration tests are not enabled!'); - } - - if (extension_loaded('pdo')) { - $this->adapters['pdo_sqlite'] = new PDO( - 'sqlite::memory' - ); - } - } } diff --git a/test/unit/AdapterTest.php b/test/unit/AdapterTest.php index abdfca2..077b82c 100644 --- a/test/unit/AdapterTest.php +++ b/test/unit/AdapterTest.php @@ -13,7 +13,7 @@ use PhpDb\ResultSet\ResultSet; use PhpDb\ResultSet\ResultSetInterface; use PhpDb\Adapter\Sqlite\Adapter; -use PhpDb\Adapter\Sqlite\Driver\Pdo\Driver; +use PhpDb\Adapter\Sqlite\Driver\Pdo\Pdo; use PhpDb\Adapter\Sqlite\Driver\Pdo\Statement; use PhpDb\Adapter\Sqlite\Platform\Sqlite as SqlitePlatform; use PhpDbTest\Adapter\Sqlite\TestAsset\TemporaryResultSet; @@ -72,7 +72,7 @@ public function testCreateDriver(): void { if (extension_loaded('pdo')) { $adapter = new Adapter(['driver' => 'pdo_sqlite'], $this->mockPlatform); - self::assertInstanceOf(Driver::class, $adapter->driver); + self::assertInstanceOf(Pdo::class, $adapter->driver); unset($adapter); } } @@ -246,7 +246,7 @@ protected function setUp(): void $this->mockConnection = $this->createMock(ConnectionInterface::class); $this->mockPlatform = new SqlitePlatform(); $this->mockStatement = $this->getMockBuilder(Statement::class)->getMock(); - $this->mockDriver = $this->getMockBuilder(Driver::class) + $this->mockDriver = $this->getMockBuilder(Pdo::class) ->setConstructorArgs([ $this->mockConnection, $this->mockStatement, diff --git a/test/unit/ConfigProviderTest.php b/test/unit/ConfigProviderTest.php index 2e6cfea..a73109f 100644 --- a/test/unit/ConfigProviderTest.php +++ b/test/unit/ConfigProviderTest.php @@ -1,16 +1,29 @@ > */ private array $config = [ 'aliases' => [ - PlatformInterface::class => Platform\Sqlite::class, - ProfilerInterface::class => Profiler::class, + MetadataInterface::class => Metadata\Source\SqliteMetadata::class, + ], + 'factories' => [ + Metadata\Source\SqliteMetadata::class => Container\MetadataInterfaceFactory::class, + ], + 'delegators' => [ + AdapterManager::class => [ + Container\AdapterManagerDelegator::class, + ], + ], + ]; + + private array $adapterManagerConfig = [ + 'aliases' => [ + 'SQLite' => Driver\Pdo\Pdo::class, + 'Sqlite' => Driver\Pdo\Pdo::class, + 'sqlite' => Driver\Pdo\Pdo::class, + 'pdo' => Driver\Pdo\Pdo::class, + 'pdo_sqlite' => Driver\Pdo\Pdo::class, + 'pdosqlite' => Driver\Pdo\Pdo::class, + 'pdodriver' => Driver\Pdo\Pdo::class, + ConnectionInterface::class => Driver\Pdo\Connection::class, + PdoConnectionInterface::class => Driver\Pdo\Connection::class, + DriverInterface::class => Driver\Pdo\Pdo::class, + PdoDriverInterface::class => Driver\Pdo\Pdo::class, + PlatformInterface::class => Platform\Sqlite::class, + ProfilerInterface::class => Profiler::class, + ResultInterface::class => Result::class, + ResultSet\ResultSetInterface::class => ResultSet\ResultSet::class, + StatementInterface::class => Statement::class, ], 'factories' => [ - AdapterInterface::class => AdapterServiceFactory::class, - DriverInterface::class => Driver\Pdo\DriverFactory::class, - Platform\Sqlite::class => InvokableFactory::class, - Profiler::class => InvokableFactory::class, + AdapterInterface::class => Container\AdapterFactory::class, + Driver\Pdo\Connection::class => Container\PdoConnectionFactory::class, + Driver\Pdo\Pdo::class => Container\PdoDriverFactory::class, + Result::class => Container\PdoResultFactory::class, + Statement::class => Container\PdoStatementFactory::class, + Platform\Sqlite::class => Container\PlatformInterfaceFactory::class, + //Profiler::class => InvokableFactory::class, + ResultSet\ResultSet::class => InvokableFactory::class, ], ]; - public function testProvidesExpectedConfiguration(): ConfigProvider + public function testProvidesExpectedDependencies(): ConfigProvider { $provider = new ConfigProvider(); self::assertEquals($this->config, $provider->getDependencies()); @@ -42,9 +88,21 @@ public function testProvidesExpectedConfiguration(): ConfigProvider return $provider; } - #[Depends('testProvidesExpectedConfiguration')] + public function testProvidesExpectedAdapterManagerConfiguration(): void + { + $provider = new ConfigProvider(); + self::assertEquals($this->adapterManagerConfig, $provider->getAdapterManagerConfig()); + } + + #[Depends('testProvidesExpectedDependencies')] public function testInvocationProvidesDependencyConfiguration(ConfigProvider $provider): void { - self::assertEquals(['dependencies' => $provider->getDependencies()], $provider()); + self::assertEquals( + [ + 'dependencies' => $provider->getDependencies(), + AdapterManager::class => $provider->getAdapterManagerConfig(), + ], + $provider() + ); } } diff --git a/test/unit/Driver/Pdo/PdoTest.php b/test/unit/Driver/Pdo/PdoTest.php index bafd913..7fabbb4 100644 --- a/test/unit/Driver/Pdo/PdoTest.php +++ b/test/unit/Driver/Pdo/PdoTest.php @@ -4,21 +4,21 @@ namespace PhpDbTest\Adapter\Sqlite\Sqlite\Driver\Pdo; -use PhpDb\Adapter\Driver\DriverInterface; +use Override; +use PhpDb\Adapter\Driver\PdoDriverInterface; use PhpDb\Adapter\Driver\Pdo\Result; use PhpDb\Exception\RuntimeException; use PhpDb\Adapter\Sqlite\Driver\Pdo\Connection; -use PhpDb\Adapter\Sqlite\Driver\Pdo\Driver; -use Override; +use PhpDb\Adapter\Sqlite\Driver\Pdo\Pdo; use PHPUnit\Framework\Attributes\CoversMethod; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; -#[CoversMethod(Driver::class, 'getDatabasePlatformName')] -#[CoversMethod(Driver::class, 'getResultPrototype')] +#[CoversMethod(Pdo::class, 'getDatabasePlatformName')] +#[CoversMethod(Pdo::class, 'getResultPrototype')] final class PdoTest extends TestCase { - protected Driver $pdo; + protected Pdo $pdo; /** * Sets up the fixture, for example, opens a network connection. @@ -29,14 +29,14 @@ protected function setUp(): void { $connection = new Connection(); - $this->pdo = new Driver($connection); + $this->pdo = new Pdo($connection); } public function testGetDatabasePlatformName(): void { $this->pdo->getConnection()->setConnectionParameters(['pdodriver' => 'pdo_sqlite']); self::assertEquals('Sqlite', $this->pdo->getDatabasePlatformName()); - self::assertEquals('SQLite', $this->pdo->getDatabasePlatformName(DriverInterface::NAME_FORMAT_NATURAL)); + self::assertEquals('SQLite', $this->pdo->getDatabasePlatformName(PdoDriverInterface::NAME_FORMAT_NATURAL)); } /** @psalm-return array */ @@ -48,11 +48,11 @@ public static function getParamsAndType(): array ['123foo', null, ':123foo'], [1, null, '?'], ['1', null, '?'], - ['foo', DriverInterface::PARAMETERIZATION_NAMED, ':foo'], - ['foo_bar', DriverInterface::PARAMETERIZATION_NAMED, ':foo_bar'], - ['123foo', DriverInterface::PARAMETERIZATION_NAMED, ':123foo'], - [1, DriverInterface::PARAMETERIZATION_NAMED, ':1'], - ['1', DriverInterface::PARAMETERIZATION_NAMED, ':1'], + ['foo', PdoDriverInterface::PARAMETERIZATION_NAMED, ':foo'], + ['foo_bar', PdoDriverInterface::PARAMETERIZATION_NAMED, ':foo_bar'], + ['123foo', PdoDriverInterface::PARAMETERIZATION_NAMED, ':123foo'], + [1, PdoDriverInterface::PARAMETERIZATION_NAMED, ':1'], + ['1', PdoDriverInterface::PARAMETERIZATION_NAMED, ':1'], [':foo', null, ':foo'], ]; } diff --git a/test/unit/Driver/Pdo/StatementIntegrationTest.php b/test/unit/Driver/Pdo/StatementIntegrationTest.php index 94b28a4..b8d8d7f 100644 --- a/test/unit/Driver/Pdo/StatementIntegrationTest.php +++ b/test/unit/Driver/Pdo/StatementIntegrationTest.php @@ -4,12 +4,11 @@ namespace PhpDbTest\Adapter\Sqlite\Driver\Pdo; -use PhpDb\Adapter\Driver\Pdo\Statement; -use PhpDb\Adapter\Sqlite\Driver\Pdo\Driver; -use PhpDbTest\Adapter\Sqlite\Driver\Pdo\TestAsset\CtorlessPdo; use Override; -use PDO; use PDOStatement; +use PhpDb\Adapter\Driver\Pdo\Statement; +use PhpDb\Adapter\Sqlite\Driver\Pdo\Pdo; +use PhpDbTest\Adapter\Sqlite\Driver\Pdo\TestAsset\CtorlessPdo; use PHPUnit\Framework\Attributes\CoversMethod; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -26,7 +25,7 @@ public function testStatementExecuteWillConvertPhpBoolToPdoBoolWhenBinding(): vo $this->pdoStatementMock->expects($this->any())->method('bindParam')->with( $this->equalTo(':foo'), $this->equalTo(false), - $this->equalTo(PDO::PARAM_BOOL) + $this->equalTo(\PDO::PARAM_BOOL) ); $this->statement->execute(['foo' => false]); } @@ -36,7 +35,7 @@ public function testStatementExecuteWillUsePdoStrByDefaultWhenBinding(): void $this->pdoStatementMock->expects($this->any())->method('bindParam')->with( $this->equalTo(':foo'), $this->equalTo('bar'), - $this->equalTo(PDO::PARAM_STR) + $this->equalTo(\PDO::PARAM_STR) ); $this->statement->execute(['foo' => 'bar']); } @@ -46,7 +45,7 @@ public function testStatementExecuteWillUsePdoStrForStringIntegerWhenBinding(): $this->pdoStatementMock->expects($this->any())->method('bindParam')->with( $this->equalTo(':foo'), $this->equalTo('123'), - $this->equalTo(PDO::PARAM_STR) + $this->equalTo(\PDO::PARAM_STR) ); $this->statement->execute(['foo' => '123']); } @@ -56,7 +55,7 @@ public function testStatementExecuteWillUsePdoIntForIntWhenBinding(): void $this->pdoStatementMock->expects($this->any())->method('bindParam')->with( $this->equalTo(':foo'), $this->equalTo(123), - $this->equalTo(PDO::PARAM_INT) + $this->equalTo(\PDO::PARAM_INT) ); $this->statement->execute(['foo' => 123]); } @@ -68,7 +67,7 @@ public function testStatementExecuteWillUsePdoIntForIntWhenBinding(): void #[Override] protected function setUp(): void { - $driver = $this->getMockBuilder(Driver::class) + $driver = $this->getMockBuilder(Pdo::class) ->onlyMethods(['createResult']) ->disableOriginalConstructor() ->getMock(); @@ -81,12 +80,4 @@ protected function setUp(): void ->getMock() )); } - - /** - * Tears down the fixture, for example, closes a network connection. - * This method is called after a test is executed. - */ - protected function tearDown(): void - { - } } diff --git a/test/unit/Driver/Pdo/StatementTest.php b/test/unit/Driver/Pdo/StatementTest.php index 2a0552b..ccf0951 100644 --- a/test/unit/Driver/Pdo/StatementTest.php +++ b/test/unit/Driver/Pdo/StatementTest.php @@ -7,7 +7,7 @@ use PhpDb\Adapter\Driver\Pdo\Statement; use PhpDb\Adapter\ParameterContainer; use PhpDb\Adapter\Sqlite\Driver\Pdo\Connection; -use PhpDb\Adapter\Sqlite\Driver\Pdo\Driver; +use PhpDb\Adapter\Sqlite\Driver\Pdo\Pdo; use Override; use PHPUnit\Framework\Attributes\CoversMethod; use PHPUnit\Framework\TestCase; diff --git a/test/unit/ModuleTest.php b/test/unit/ModuleTest.php deleted file mode 100644 index 091ff6e..0000000 --- a/test/unit/ModuleTest.php +++ /dev/null @@ -1,42 +0,0 @@ -> */ - private array $config = [ - 'aliases' => [ - PlatformInterface::class => Platform\Sqlite::class, - ProfilerInterface::class => Profiler::class, - ], - 'factories' => [ - AdapterInterface::class => AdapterServiceFactory::class, - DriverInterface::class => Driver\Pdo\DriverFactory::class, - Platform\Sqlite::class => InvokableFactory::class, - Profiler::class => InvokableFactory::class, - ], - ]; - - public function testProvidesExpectedConfiguration(): Module - { - $provider = new Module(); - self::assertEquals(['service_manager' => $this->config], $provider->getConfig()); - - return $provider; - } -} diff --git a/test/unit/Platform/SqliteTest.php b/test/unit/Platform/SqliteTest.php index ca5e9be..8053a11 100644 --- a/test/unit/Platform/SqliteTest.php +++ b/test/unit/Platform/SqliteTest.php @@ -3,7 +3,7 @@ namespace PhpDbTest\Adapter\Sqlite\Sqlite\Platform; use PhpDb\Adapter\Sqlite\Driver\Pdo\Connection; -use PhpDb\Adapter\Sqlite\Driver\Pdo\Driver; +use PhpDb\Adapter\Sqlite\Driver\Pdo\Pdo; use PhpDb\Adapter\Sqlite\Platform\Sqlite; use Override; use PHPUnit\Framework\Attributes\CoversMethod; diff --git a/test/unit/Sql/Platform/SelectDecoratorTest.php b/test/unit/Sql/Platform/SelectDecoratorTest.php index 34446b5..b93cf03 100644 --- a/test/unit/Sql/Platform/SelectDecoratorTest.php +++ b/test/unit/Sql/Platform/SelectDecoratorTest.php @@ -1,30 +1,51 @@ driver = $this->getMockBuilder(PdoDriverInterface::class) + ->getMock(); + + $this->platform = $this->getMockBuilder(SqlitePlatform::class) + ->onlyMethods([]) + ->setConstructorArgs([$this->driver]) + ->getMock(); + + parent::setUp(); + } + public function testLimitAndOffset(): void { $select = new Select(); $select->offset(5); $selectDecorator = new SelectDecorator(); $selectDecorator->setSubject($select); - $sqlString = $selectDecorator->getSqlString(new SqlitePlatform()); + $sqlString = $selectDecorator->getSqlString($this->platform); self::assertEquals('SELECT * LIMIT 18446744073709551615 OFFSET 5', $sqlString); } @@ -40,11 +61,11 @@ public function testPrepareStatementPreparesUnionSyntaxFromCombine( $driver->expects($this->any())->method('formatParameterName')->willReturn('?'); // test - $adapter = $this->getMockBuilder(Adapter::class) + $adapter = $this->getMockBuilder(AdapterInterface::class) ->onlyMethods([]) ->setConstructorArgs([ $driver, - new SqlitePlatform(), + $this->platform, ]) ->getMock(); @@ -76,7 +97,7 @@ public function testGetSqlStringPreparesUnionSyntaxFromCombine( $selectDecorator = new SelectDecorator(); $selectDecorator->setSubject($select); - self::assertEquals($expectedSql, $selectDecorator->getSqlString(new SqlitePlatform())); + self::assertEquals($expectedSql, $selectDecorator->getSqlString($this->platform)); } /**