Skip to content

Commit

Permalink
add PDO Sqlite driver WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
hrach committed Oct 13, 2021
1 parent b18e3d0 commit 5b4e69e
Show file tree
Hide file tree
Showing 22 changed files with 1,273 additions and 4 deletions.
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

0 comments on commit 5b4e69e

Please sign in to comment.