Skip to content

Commit 5b4e69e

Browse files
committed
add PDO Sqlite driver WIP
1 parent b18e3d0 commit 5b4e69e

22 files changed

+1273
-4
lines changed

.idea/sqldialects.xml

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/datetime.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ The following table presents a matrix of available DB date-time types:
1616
| MySQL | `datetime` | `timestamp` | - |
1717
| Postgres | `timestamp` | `timestamptz` | - |
1818
| SQL Server | `datetime`, `datetime2` | - | `datetimeoffset` |
19+
| Sqlite | - | - | -
1920

2021
- **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;
2122
- **timezone conversion**: database stores the time-stamp unified in UTC and reads it in connection's timezone;
@@ -90,3 +91,25 @@ This will make Dbal fully functional, although some SQL queries and expressions
9091
|------|-------------|--------
9192
| local datetime | `datetime` | value is converted into application timezone
9293
| datetime | `datetimeoffset` | value is read with timezone offset and no further modification is done - i.e. no application timezone conversion happens
94+
95+
--------------------------
96+
97+
### Sqlite
98+
99+
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.
100+
101+
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).
102+
103+
##### Writing
104+
105+
| Type | Modifier | Comment
106+
|------|----------|--------
107+
| local datetime | `%ldt` | the timezone offset is removed and value is formatter as ISO string without the timezone offset
108+
| datetime | `%dt` | no timezone conversion is done and value is formatted as ISO string with timezone offset
109+
110+
##### Reading
111+
112+
| Type | Column Type | Comment
113+
|--------------------|-------------|--------
114+
| local datetime | ❌ | cannot be auto-detected
115+
| datetime | ❌ | cannot be auto-detected

docs/default.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,16 @@ Supported platforms:
66

77
- **MySQL** via `mysqli` or `pdo_mysql` extension,
88
- **Postgres** via `pgsql` or `pdo_pgsql` extension,
9-
- **MS SQL Server** via `sqlsrv` or `pdo_sqlsrv` extension.
9+
- **MS SQL Server** via `sqlsrv` or `pdo_sqlsrv` extension,
10+
- **Sqlite** via `pdo_sqlite` extension.
1011

1112
### Connection
1213

1314
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.
1415

15-
| Key | Description
16-
| --- | --- |
17-
| `driver` | driver name, use `mysqli`, `pgsql`, `sqlsrv`, `pdo_mysql`, `pdo_pgsql`, `pdo_sqlsrv`
16+
| Key | Description
17+
| --- | --- |
18+
| `driver` | driver name, use `mysqli`, `pgsql`, `sqlsrv`, `pdo_mysql`, `pdo_pgsql`, `pdo_sqlsrv`, `pdo_sqlite`
1819
| `host` | database server name
1920
| `username` | username for authentication
2021
| `password` | password for authentication

readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Supported platforms:
1212
- **MySQL** via `mysqli` or `pdo_mysql` extension,
1313
- **PostgreSQL** via `pgsql` or `pdo_pgsql` extension,
1414
- **MS SQL Server** via `sqlsrv` or `pdo_sqlsrv` extension.
15+
- **Sqlite** via `pdo_sqlite` extension.
1516

1617
Integrations:
1718
- Symfony Bundle
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Nextras\Dbal\Drivers\PdoSqlite;
4+
5+
6+
use DateTimeZone;
7+
use Exception;
8+
use Nextras\Dbal\Connection;
9+
use Nextras\Dbal\Drivers\Exception\ConnectionException;
10+
use Nextras\Dbal\Drivers\Exception\DriverException;
11+
use Nextras\Dbal\Drivers\Exception\ForeignKeyConstraintViolationException;
12+
use Nextras\Dbal\Drivers\Exception\NotNullConstraintViolationException;
13+
use Nextras\Dbal\Drivers\Exception\QueryException;
14+
use Nextras\Dbal\Drivers\Exception\UniqueConstraintViolationException;
15+
use Nextras\Dbal\Drivers\Pdo\PdoDriver;
16+
use Nextras\Dbal\Exception\NotSupportedException;
17+
use Nextras\Dbal\IConnection;
18+
use Nextras\Dbal\ILogger;
19+
use Nextras\Dbal\Platforms\IPlatform;
20+
use Nextras\Dbal\Platforms\SqlitePlatform;
21+
use Nextras\Dbal\Result\IResultAdapter;
22+
use PDOStatement;
23+
24+
25+
/**
26+
* Driver for php_pdo_sqlite ext.
27+
*
28+
* Supported configuration options:
29+
* - filename - file path to database or `:memory:`; defaults to :memory:
30+
*/
31+
class PdoSqliteDriver extends PdoDriver
32+
{
33+
/** @var PdoSqliteResultNormalizerFactory */
34+
private $resultNormalizerFactory;
35+
36+
37+
public function connect(array $params, ILogger $logger): void
38+
{
39+
$file = $params['filename'] ?? ':memory:';
40+
$dsn = "sqlite:$file";
41+
$this->connectPdo($dsn, '', '', [], $logger);
42+
$this->resultNormalizerFactory = new PdoSqliteResultNormalizerFactory();
43+
44+
$this->connectionTz = new DateTimeZone('UTC');
45+
$this->loggedQuery('PRAGMA foreign_keys = 1');
46+
}
47+
48+
49+
public function createPlatform(IConnection $connection): IPlatform
50+
{
51+
return new SqlitePlatform($connection);
52+
}
53+
54+
55+
public function getLastInsertedId(?string $sequenceName = null)
56+
{
57+
return $this->query('SELECT last_insert_rowid()')->fetchField();
58+
}
59+
60+
61+
public function setTransactionIsolationLevel(int $level): void
62+
{
63+
static $levels = [
64+
Connection::TRANSACTION_READ_UNCOMMITTED => 'READ UNCOMMITTED',
65+
Connection::TRANSACTION_READ_COMMITTED => 'READ COMMITTED',
66+
Connection::TRANSACTION_REPEATABLE_READ => 'REPEATABLE READ',
67+
Connection::TRANSACTION_SERIALIZABLE => 'SERIALIZABLE',
68+
];
69+
if (!isset($levels[$level])) {
70+
throw new NotSupportedException("Unsupported transaction level $level");
71+
}
72+
$this->loggedQuery("SET SESSION TRANSACTION ISOLATION LEVEL {$levels[$level]}");
73+
}
74+
75+
76+
protected function createResultAdapter(PDOStatement $statement): IResultAdapter
77+
{
78+
return (new PdoSqliteResultAdapter($statement, $this->resultNormalizerFactory))->toBuffered();
79+
}
80+
81+
82+
protected function convertIdentifierToSql(string $identifier): string
83+
{
84+
return '[' . strtr($identifier, '[]', ' ') . ']';
85+
}
86+
87+
88+
protected function createException(string $error, int $errorNo, string $sqlState, ?string $query = null): Exception
89+
{
90+
if (stripos($error, 'FOREIGN KEY constraint failed') !== false) {
91+
return new ForeignKeyConstraintViolationException($error, $errorNo, '', null, $query);
92+
} elseif (
93+
strpos($error, 'must be unique') !== false
94+
|| strpos($error, 'is not unique') !== false
95+
|| strpos($error, 'are not unique') !== false
96+
|| strpos($error, 'UNIQUE constraint failed') !== false
97+
) {
98+
return new UniqueConstraintViolationException($error, $errorNo, '', null, $query);
99+
} elseif (
100+
strpos($error, 'may not be NULL') !== false
101+
|| strpos($error, 'NOT NULL constraint failed') !== false
102+
) {
103+
return new NotNullConstraintViolationException($error, $errorNo, '', null, $query);
104+
} elseif (stripos($error, 'unable to open database') !== false) {
105+
return new ConnectionException($error, $errorNo, '');
106+
} elseif ($query !== null) {
107+
return new QueryException($error, $errorNo, '', null, $query);
108+
} else {
109+
return new DriverException($error, $errorNo, '');
110+
}
111+
}
112+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Nextras\Dbal\Drivers\PdoSqlite;
4+
5+
6+
use Nextras\Dbal\Exception\NotSupportedException;
7+
use Nextras\Dbal\Result\FullyBufferedResultAdapter;
8+
use Nextras\Dbal\Result\IResultAdapter;
9+
use Nextras\Dbal\Utils\StrictObjectTrait;
10+
use PDO;
11+
use PDOStatement;
12+
use function strtolower;
13+
14+
15+
class PdoSqliteResultAdapter implements IResultAdapter
16+
{
17+
use StrictObjectTrait;
18+
19+
20+
/** @var PDOStatement<mixed> */
21+
private $statement;
22+
23+
/** @var bool */
24+
private $beforeFirstFetch = true;
25+
26+
/** @var PdoSqliteResultNormalizerFactory */
27+
private $normalizerFactory;
28+
29+
30+
/**
31+
* @param PDOStatement<mixed> $statement
32+
*/
33+
public function __construct(PDOStatement $statement, PdoSqliteResultNormalizerFactory $normalizerFactory)
34+
{
35+
$this->statement = $statement;
36+
$this->normalizerFactory = $normalizerFactory;
37+
}
38+
39+
40+
public function toBuffered(): IResultAdapter
41+
{
42+
return new FullyBufferedResultAdapter($this);
43+
}
44+
45+
46+
public function toUnbuffered(): IResultAdapter
47+
{
48+
return $this;
49+
}
50+
51+
52+
public function seek(int $index): void
53+
{
54+
if ($index === 0 && $this->beforeFirstFetch) {
55+
return;
56+
}
57+
58+
throw new NotSupportedException("PDO does not support rewinding or seeking. Use Result::buffered() before first consume of the result.");
59+
}
60+
61+
62+
public function fetch(): ?array
63+
{
64+
$this->beforeFirstFetch = false;
65+
$fetched = $this->statement->fetch(PDO::FETCH_ASSOC);
66+
return $fetched !== false ? $fetched : null;
67+
}
68+
69+
70+
public function getRowsCount(): int
71+
{
72+
return $this->statement->rowCount();
73+
}
74+
75+
76+
public function getTypes(): array
77+
{
78+
$types = [];
79+
$count = $this->statement->columnCount();
80+
81+
for ($i = 0; $i < $count; $i++) {
82+
$field = $this->statement->getColumnMeta($i);
83+
if ($field === false) { // @phpstan-ignore-line
84+
// Sqlite does not return meta for special queries (PRAGMA, etc.)
85+
continue;
86+
}
87+
88+
$type = strtolower($field['sqlite:decl_type'] ?? $field['native_type'] ?? '');
89+
$types[(string) $field['name']] = $type;
90+
}
91+
92+
return $types;
93+
}
94+
95+
96+
public function getNormalizers(): array
97+
{
98+
return $this->normalizerFactory->resolve($this->getTypes());
99+
}
100+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Nextras\Dbal\Drivers\PdoSqlite;
4+
5+
6+
use Closure;
7+
use Nextras\Dbal\Utils\StrictObjectTrait;
8+
9+
10+
/**
11+
* @internal
12+
*/
13+
class PdoSqliteResultNormalizerFactory
14+
{
15+
use StrictObjectTrait;
16+
17+
18+
/** @var Closure(mixed): mixed */
19+
private $intNormalizer;
20+
21+
/** @var Closure(mixed): mixed */
22+
private $floatNormalizer;
23+
24+
25+
public function __construct()
26+
{
27+
$this->intNormalizer = static function ($value): ?int {
28+
if ($value === null) return null;
29+
return (int) $value;
30+
};
31+
32+
$this->floatNormalizer = static function ($value): ?float {
33+
if ($value === null) return null;
34+
return (float) $value;
35+
};
36+
}
37+
38+
39+
/**
40+
* @param array<string, mixed> $types
41+
* @return array<string, callable (mixed): mixed>
42+
*/
43+
public function resolve(array $types): array
44+
{
45+
static $ints = [
46+
'int' => true,
47+
'integer' => true,
48+
'tinyint' => true,
49+
'smallint' => true,
50+
'mediumint' => true,
51+
'bigint' => true,
52+
'unsigned big int' => true,
53+
'int2' => true,
54+
'int8' => true,
55+
];
56+
57+
static $floats = [
58+
'real' => self::TYPE_FLOAT,
59+
'double' => self::TYPE_FLOAT,
60+
'double precision' => self::TYPE_FLOAT,
61+
'float' => self::TYPE_FLOAT,
62+
'numeric' => self::TYPE_FLOAT,
63+
'decimal' => self::TYPE_FLOAT,
64+
];
65+
66+
$normalizers = [];
67+
foreach ($types as $column => $type) {
68+
if ($type === 'text' || $type === 'varchar') {
69+
continue; // optimization
70+
} elseif ($type === 'integer') {
71+
$normalizers[$column] = $this->intNormalizer;
72+
} elseif ($type === 'real') {
73+
$normalizers[$column] = $this->floatNormalizer;
74+
}
75+
}
76+
return $normalizers;
77+
}
78+
}

src/Platforms/IPlatform.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ interface IPlatform
1515
public const SUPPORT_MULTI_COLUMN_IN = 1;
1616
public const SUPPORT_QUERY_EXPLAIN = 2;
1717
public const SUPPORT_WHITESPACE_EXPLAIN = 3;
18+
public const SUPPORT_INSERT_DEFAULT_KEYWORD = 4;
1819

1920

2021
/**

0 commit comments

Comments
 (0)