Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Sqlite PDO driver #130

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .idea/sqldialects.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions docs/datetime.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ The following table presents a matrix of available DB date-time types:
| MySQL | `datetime` | `timestamp` | - |
| Postgres | `timestamp` | `timestamptz` | - |
| SQL Server | `datetime`, `datetime2` | - | `datetimeoffset` |
| Sqlite | - | - | -

- **no timezone handling**: database stores the time-stamp and does not do any modification to it; this is the easiest solution, but brings a disadvantage: database cannot exactly diff two time-stamps, i.e. it may produce wrong results because day-light saving shift is needed but db does not know which zone to use for the calculation;
- **timezone conversion**: database stores the time-stamp unified in UTC and reads it in connection's timezone;
Expand Down Expand Up @@ -90,3 +91,25 @@ This will make Dbal fully functional, although some SQL queries and expressions
|------|-------------|--------
| local datetime | `datetime` | value is converted into application timezone
| datetime | `datetimeoffset` | value is read with timezone offset and no further modification is done - i.e. no application timezone conversion happens

--------------------------

### Sqlite

Sqlite does not have a dedicated type for date time at all. However, Sqlite provides a function that helps to transform unix time to a local time zone.

Use `datetime(your_column, 'unixepoch', 'localtime')` to convert stored timestamp to your local time-zone. Read more in the [official documentation](https://sqlite.org/lang_datefunc.html#modifiers).

##### Writing

| Type | Modifier | Comment
|------|----------|--------
| local datetime | `%ldt` | the timezone offset is removed and value is formatter as ISO string without the timezone offset
| datetime | `%dt` | no timezone conversion is done and value is formatted as ISO string with timezone offset

##### Reading

| Type | Column Type | Comment
|--------------------|-------------|--------
| local datetime | ❌ | cannot be auto-detected
| datetime | ❌ | cannot be auto-detected
9 changes: 5 additions & 4 deletions docs/default.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@ Supported platforms:

- **MySQL** via `mysqli` or `pdo_mysql` extension,
- **Postgres** via `pgsql` or `pdo_pgsql` extension,
- **MS SQL Server** via `sqlsrv` or `pdo_sqlsrv` extension.
- **MS SQL Server** via `sqlsrv` or `pdo_sqlsrv` extension,
- **Sqlite** via `pdo_sqlite` extension.

### Connection

The Connection instance is the main access point to the database. Connection's constructor accepts a configuration array. The possible keys depend on the specific driver; some configuration keys are shared for all drivers. To actual list of supported keys are enumerated in PhpDoc comment in driver's source code.

| Key | Description
| --- | --- |
| `driver` | driver name, use `mysqli`, `pgsql`, `sqlsrv`, `pdo_mysql`, `pdo_pgsql`, `pdo_sqlsrv`
| Key | Description
| --- | --- |
| `driver` | driver name, use `mysqli`, `pgsql`, `sqlsrv`, `pdo_mysql`, `pdo_pgsql`, `pdo_sqlsrv`, `pdo_sqlite`
| `host` | database server name
| `username` | username for authentication
| `password` | password for authentication
Expand Down
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Supported platforms:
- **MySQL** via `mysqli` or `pdo_mysql` extension,
- **PostgreSQL** via `pgsql` or `pdo_pgsql` extension,
- **MS SQL Server** via `sqlsrv` or `pdo_sqlsrv` extension.
- **Sqlite** via `pdo_sqlite` extension.

Integrations:
- Symfony Bundle
Expand Down
112 changes: 112 additions & 0 deletions src/Drivers/PdoSqlite/PdoSqliteDriver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php declare(strict_types = 1);

namespace Nextras\Dbal\Drivers\PdoSqlite;


use DateTimeZone;
use Exception;
use Nextras\Dbal\Connection;
use Nextras\Dbal\Drivers\Exception\ConnectionException;
use Nextras\Dbal\Drivers\Exception\DriverException;
use Nextras\Dbal\Drivers\Exception\ForeignKeyConstraintViolationException;
use Nextras\Dbal\Drivers\Exception\NotNullConstraintViolationException;
use Nextras\Dbal\Drivers\Exception\QueryException;
use Nextras\Dbal\Drivers\Exception\UniqueConstraintViolationException;
use Nextras\Dbal\Drivers\Pdo\PdoDriver;
use Nextras\Dbal\Exception\NotSupportedException;
use Nextras\Dbal\IConnection;
use Nextras\Dbal\ILogger;
use Nextras\Dbal\Platforms\IPlatform;
use Nextras\Dbal\Platforms\SqlitePlatform;
use Nextras\Dbal\Result\IResultAdapter;
use PDOStatement;


/**
* Driver for php_pdo_sqlite ext.
*
* Supported configuration options:
* - filename - file path to database or `:memory:`; defaults to :memory:
*/
class PdoSqliteDriver extends PdoDriver
{
/** @var PdoSqliteResultNormalizerFactory */
private $resultNormalizerFactory;


public function connect(array $params, ILogger $logger): void
{
$file = $params['filename'] ?? ':memory:';
$dsn = "sqlite:$file";
$this->connectPdo($dsn, '', '', [], $logger);
$this->resultNormalizerFactory = new PdoSqliteResultNormalizerFactory();

$this->connectionTz = new DateTimeZone('UTC');
$this->loggedQuery('PRAGMA foreign_keys = 1');
}


public function createPlatform(IConnection $connection): IPlatform
{
return new SqlitePlatform($connection);
}


public function getLastInsertedId(?string $sequenceName = null)
{
return $this->query('SELECT last_insert_rowid()')->fetchField();
}


public function setTransactionIsolationLevel(int $level): void
{
static $levels = [
Connection::TRANSACTION_READ_UNCOMMITTED => 'READ UNCOMMITTED',
Connection::TRANSACTION_READ_COMMITTED => 'READ COMMITTED',
Connection::TRANSACTION_REPEATABLE_READ => 'REPEATABLE READ',
Connection::TRANSACTION_SERIALIZABLE => 'SERIALIZABLE',
];
if (!isset($levels[$level])) {
throw new NotSupportedException("Unsupported transaction level $level");
}
$this->loggedQuery("SET SESSION TRANSACTION ISOLATION LEVEL {$levels[$level]}");
}


protected function createResultAdapter(PDOStatement $statement): IResultAdapter
{
return (new PdoSqliteResultAdapter($statement, $this->resultNormalizerFactory))->toBuffered();
}


protected function convertIdentifierToSql(string $identifier): string
{
return '[' . strtr($identifier, '[]', ' ') . ']';
}


protected function createException(string $error, int $errorNo, string $sqlState, ?string $query = null): Exception
{
if (stripos($error, 'FOREIGN KEY constraint failed') !== false) {
return new ForeignKeyConstraintViolationException($error, $errorNo, '', null, $query);
} elseif (
strpos($error, 'must be unique') !== false
|| strpos($error, 'is not unique') !== false
|| strpos($error, 'are not unique') !== false
|| strpos($error, 'UNIQUE constraint failed') !== false
) {
return new UniqueConstraintViolationException($error, $errorNo, '', null, $query);
} elseif (
strpos($error, 'may not be NULL') !== false
|| strpos($error, 'NOT NULL constraint failed') !== false
) {
return new NotNullConstraintViolationException($error, $errorNo, '', null, $query);
} elseif (stripos($error, 'unable to open database') !== false) {
return new ConnectionException($error, $errorNo, '');
} elseif ($query !== null) {
return new QueryException($error, $errorNo, '', null, $query);
} else {
return new DriverException($error, $errorNo, '');
}
}
}
100 changes: 100 additions & 0 deletions src/Drivers/PdoSqlite/PdoSqliteResultAdapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php declare(strict_types = 1);

namespace Nextras\Dbal\Drivers\PdoSqlite;


use Nextras\Dbal\Exception\NotSupportedException;
use Nextras\Dbal\Result\FullyBufferedResultAdapter;
use Nextras\Dbal\Result\IResultAdapter;
use Nextras\Dbal\Utils\StrictObjectTrait;
use PDO;
use PDOStatement;
use function strtolower;


class PdoSqliteResultAdapter implements IResultAdapter
{
use StrictObjectTrait;


/** @var PDOStatement<mixed> */
private $statement;

/** @var bool */
private $beforeFirstFetch = true;

/** @var PdoSqliteResultNormalizerFactory */
private $normalizerFactory;


/**
* @param PDOStatement<mixed> $statement
*/
public function __construct(PDOStatement $statement, PdoSqliteResultNormalizerFactory $normalizerFactory)
{
$this->statement = $statement;
$this->normalizerFactory = $normalizerFactory;
}


public function toBuffered(): IResultAdapter
{
return new FullyBufferedResultAdapter($this);
}


public function toUnbuffered(): IResultAdapter
{
return $this;
}


public function seek(int $index): void
{
if ($index === 0 && $this->beforeFirstFetch) {
return;
}

throw new NotSupportedException("PDO does not support rewinding or seeking. Use Result::buffered() before first consume of the result.");
}


public function fetch(): ?array
{
$this->beforeFirstFetch = false;
$fetched = $this->statement->fetch(PDO::FETCH_ASSOC);
return $fetched !== false ? $fetched : null;
}


public function getRowsCount(): int
{
return $this->statement->rowCount();
}


public function getTypes(): array
{
$types = [];
$count = $this->statement->columnCount();

for ($i = 0; $i < $count; $i++) {
$field = $this->statement->getColumnMeta($i);
if ($field === false) { // @phpstan-ignore-line
// Sqlite does not return meta for special queries (PRAGMA, etc.)
continue;
}

$type = strtolower($field['sqlite:decl_type'] ?? $field['native_type'] ?? '');
$types[(string) $field['name']] = $type;
}

return $types;
}


public function getNormalizers(): array
{
return $this->normalizerFactory->resolve($this->getTypes());
}
}
78 changes: 78 additions & 0 deletions src/Drivers/PdoSqlite/PdoSqliteResultNormalizerFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php declare(strict_types = 1);

namespace Nextras\Dbal\Drivers\PdoSqlite;


use Closure;
use Nextras\Dbal\Utils\StrictObjectTrait;


/**
* @internal
*/
class PdoSqliteResultNormalizerFactory
{
use StrictObjectTrait;


/** @var Closure(mixed): mixed */
private $intNormalizer;

/** @var Closure(mixed): mixed */
private $floatNormalizer;


public function __construct()
{
$this->intNormalizer = static function ($value): ?int {
if ($value === null) return null;
return (int) $value;
};

$this->floatNormalizer = static function ($value): ?float {
if ($value === null) return null;
return (float) $value;
};
}


/**
* @param array<string, mixed> $types
* @return array<string, callable (mixed): mixed>
*/
public function resolve(array $types): array
{
static $ints = [
'int' => true,
'integer' => true,
'tinyint' => true,
'smallint' => true,
'mediumint' => true,
'bigint' => true,
'unsigned big int' => true,
'int2' => true,
'int8' => true,
];

static $floats = [
'real' => self::TYPE_FLOAT,
'double' => self::TYPE_FLOAT,
'double precision' => self::TYPE_FLOAT,
'float' => self::TYPE_FLOAT,
'numeric' => self::TYPE_FLOAT,
'decimal' => self::TYPE_FLOAT,
];

$normalizers = [];
foreach ($types as $column => $type) {
if ($type === 'text' || $type === 'varchar') {
continue; // optimization
} elseif ($type === 'integer') {
$normalizers[$column] = $this->intNormalizer;
} elseif ($type === 'real') {
$normalizers[$column] = $this->floatNormalizer;
}
}
return $normalizers;
}
}
1 change: 1 addition & 0 deletions src/Platforms/IPlatform.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface IPlatform
public const SUPPORT_MULTI_COLUMN_IN = 1;
public const SUPPORT_QUERY_EXPLAIN = 2;
public const SUPPORT_WHITESPACE_EXPLAIN = 3;
public const SUPPORT_INSERT_DEFAULT_KEYWORD = 4;


/**
Expand Down
Loading