337 lines
9.2 KiB
PHP
337 lines
9.2 KiB
PHP
<?php
|
|
namespace nulib\db\sqlite;
|
|
|
|
use Generator;
|
|
use nulib\cl;
|
|
use nulib\db\_private\_config;
|
|
use nulib\db\_private\Tvalues;
|
|
use nulib\db\IDatabase;
|
|
use nulib\db\ITransactor;
|
|
use nulib\php\func;
|
|
use nulib\ValueException;
|
|
use SQLite3;
|
|
use SQLite3Result;
|
|
use SQLite3Stmt;
|
|
|
|
/**
|
|
* Class Sqlite: frontend vers une base de données sqlite3
|
|
*/
|
|
class Sqlite implements IDatabase {
|
|
use Tvalues;
|
|
|
|
static function with($sqlite, ?array $params=null): self {
|
|
if ($sqlite instanceof static) {
|
|
return $sqlite;
|
|
} elseif ($sqlite instanceof self) {
|
|
# recréer avec les mêmes paramètres
|
|
return new static(null, cl::merge([
|
|
"file" => $sqlite->file,
|
|
"flags" => $sqlite->flags,
|
|
"encryption_key" => $sqlite->encryptionKey,
|
|
"allow_wal" => $sqlite->allowWal,
|
|
"config" => $sqlite->config,
|
|
"migrate" => $sqlite->migration,
|
|
], $params));
|
|
} elseif (is_array($sqlite)) {
|
|
return new static(null, cl::merge($sqlite, $params));
|
|
} else {
|
|
return new static($sqlite, $params);
|
|
}
|
|
}
|
|
|
|
static function config_enableExceptions(self $sqlite): void {
|
|
$sqlite->db->enableExceptions(true);
|
|
}
|
|
const CONFIG_enableExceptions = [self::class, "config_enableExceptions"];
|
|
|
|
/**
|
|
* @var int temps maximum à attendre que la base soit accessible si elle est
|
|
* verrouillée
|
|
*/
|
|
protected const BUSY_TIMEOUT = 30 * 1000;
|
|
|
|
static function config_busyTimeout(self $sqlite): void {
|
|
$sqlite->db->busyTimeout(static::BUSY_TIMEOUT);
|
|
}
|
|
const CONFIG_busyTimeout = [self::class, "config_busyTimeout"];
|
|
|
|
static function config_enableWalIfAllowed(self $sqlite): void {
|
|
if ($sqlite->isWalAllowed()) {
|
|
$sqlite->db->exec("PRAGMA journal_mode=WAL");
|
|
}
|
|
}
|
|
const CONFIG_enableWalIfAllowed = [self::class, "config_enableWalIfAllowed"];
|
|
|
|
const ALLOW_WAL = null;
|
|
|
|
const DEFAULT_CONFIG = [
|
|
self::CONFIG_enableExceptions,
|
|
self::CONFIG_busyTimeout,
|
|
self::CONFIG_enableWalIfAllowed,
|
|
];
|
|
|
|
const CONFIG = null;
|
|
|
|
const MIGRATE = null;
|
|
|
|
const params_SCHEMA = [
|
|
"file" => ["string", ""],
|
|
"flags" => ["int", SQLITE3_OPEN_READWRITE + SQLITE3_OPEN_CREATE],
|
|
"encryption_key" => ["string", ""],
|
|
"allow_wal" => ["?bool"],
|
|
"replace_config" => ["?array|callable"],
|
|
"config" => ["?array|callable"],
|
|
"migrate" => ["?array|string|callable"],
|
|
"auto_open" => ["bool", true],
|
|
];
|
|
|
|
function __construct(?string $file=null, ?array $params=null) {
|
|
if ($file !== null) $params["file"] = $file;
|
|
##schéma
|
|
$defaultFile = self::params_SCHEMA["file"][1];
|
|
$this->file = $file = strval($params["file"] ?? $defaultFile);
|
|
$inMemory = $file === ":memory:" || $file === "";
|
|
#
|
|
$defaultFlags = self::params_SCHEMA["flags"][1];
|
|
$this->flags = intval($params["flags"] ?? $defaultFlags);
|
|
#
|
|
$defaultEncryptionKey = self::params_SCHEMA["encryption_key"][1];
|
|
$this->encryptionKey = strval($params["encryption_key"] ?? $defaultEncryptionKey);
|
|
#
|
|
$defaultAllowWal = static::ALLOW_WAL ?? !$inMemory;
|
|
$this->allowWal = $params["allow_wal"] ?? $defaultAllowWal;
|
|
# configuration
|
|
$config = $params["replace_config"] ?? null;
|
|
if ($config === null) {
|
|
$config = $params["config"] ?? static::CONFIG;
|
|
if (is_callable($config)) $config = [$config];
|
|
$config = cl::merge(static::DEFAULT_CONFIG, $config);
|
|
}
|
|
$this->config = $config;
|
|
# migrations
|
|
$this->migration = $params["migrate"] ?? static::MIGRATE;
|
|
#
|
|
$defaultAutoOpen = self::params_SCHEMA["auto_open"][1];
|
|
$this->inTransaction = false;
|
|
if ($params["auto_open"] ?? $defaultAutoOpen) {
|
|
$this->open();
|
|
}
|
|
}
|
|
|
|
/** @var string */
|
|
protected $file;
|
|
|
|
/** @var int */
|
|
protected $flags;
|
|
|
|
/** @var string */
|
|
protected $encryptionKey;
|
|
|
|
/** @var bool */
|
|
protected $allowWal;
|
|
|
|
/** vérifier s'il est autorisé de configurer le mode WAL */
|
|
function isWalAllowed(): bool {
|
|
return $this->allowWal;
|
|
}
|
|
|
|
/** @var array|string|callable */
|
|
protected $config;
|
|
|
|
/** @var array|string|callable */
|
|
protected $migration;
|
|
|
|
/** @var SQLite3 */
|
|
protected $db;
|
|
|
|
protected bool $inTransaction;
|
|
|
|
function open(): self {
|
|
if ($this->db === null) {
|
|
$this->db = new SQLite3($this->file, $this->flags, $this->encryptionKey);
|
|
_config::with($this->config)->configure($this);
|
|
_migration::with($this->migration)->migrate($this);
|
|
$this->inTransaction = false;
|
|
}
|
|
return $this;
|
|
}
|
|
|
|
function close(): void {
|
|
if ($this->db !== null) {
|
|
$this->db->close();
|
|
$this->db = null;
|
|
$this->inTransaction = false;
|
|
}
|
|
}
|
|
|
|
protected function checkStmt($stmt): SQLite3Stmt {
|
|
return SqliteException::check($this->db, $stmt);
|
|
}
|
|
|
|
protected function checkResult($result): SQLite3Result {
|
|
return SqliteException::check($this->db, $result);
|
|
}
|
|
|
|
protected function db(): SQLite3 {
|
|
$this->open();
|
|
return $this->db;
|
|
}
|
|
|
|
function _exec(string $query): bool {
|
|
return $this->db()->exec($query);
|
|
}
|
|
|
|
private static function is_insert(?string $sql): bool {
|
|
if ($sql === null) return false;
|
|
return preg_match('/^\s*insert\b/i', $sql);
|
|
}
|
|
|
|
function exec($query, ?array $params=null) {
|
|
$db = $this->db();
|
|
$query = new query($query, $params);
|
|
if ($query->useStmt($db, $stmt, $sql)) {
|
|
try {
|
|
$result = $stmt->execute();
|
|
if ($result === false) return false;
|
|
$result->finalize();
|
|
if ($query->isInsert()) return $db->lastInsertRowID();
|
|
else return $db->changes();
|
|
} finally {
|
|
$stmt->close();
|
|
}
|
|
} else {
|
|
$result = $db->exec($sql);
|
|
if ($result === false) return false;
|
|
if (self::is_insert($sql)) return $db->lastInsertRowID();
|
|
else return $db->changes();
|
|
}
|
|
}
|
|
|
|
/** @var ITransactor[] */
|
|
protected ?array $transactors = null;
|
|
|
|
function willUpdate(...$transactors): self {
|
|
foreach ($transactors as $transactor) {
|
|
if ($transactor instanceof ITransactor) {
|
|
$this->transactors[] = $transactor;
|
|
$transactor->willUpdate();
|
|
} else {
|
|
throw ValueException::invalid_type($transactor, ITransactor::class);
|
|
}
|
|
}
|
|
return $this;
|
|
}
|
|
|
|
function inTransaction(): bool {
|
|
#XXX très imparfait, mais y'a rien de mieux pour le moment :-(
|
|
return $this->inTransaction;
|
|
}
|
|
|
|
function beginTransaction(?callable $func=null, bool $commit=true): void {
|
|
$this->db()->exec("begin");
|
|
$this->inTransaction = true;
|
|
if ($this->transactors !== null) {
|
|
foreach ($this->transactors as $transactor) {
|
|
$transactor->beginTransaction();
|
|
}
|
|
}
|
|
if ($func !== null) {
|
|
$commited = false;
|
|
try {
|
|
func::call($func, $this);
|
|
if ($commit) {
|
|
$this->commit();
|
|
$commited = true;
|
|
}
|
|
} finally {
|
|
if ($commit && !$commited) $this->rollback();
|
|
}
|
|
}
|
|
}
|
|
|
|
function commit(): void {
|
|
$this->inTransaction = false;
|
|
$this->db()->exec("commit");
|
|
if ($this->transactors !== null) {
|
|
foreach ($this->transactors as $transactor) {
|
|
$transactor->commit();
|
|
}
|
|
}
|
|
}
|
|
|
|
function rollback(): void {
|
|
$this->inTransaction = false;
|
|
$this->db()->exec("rollback");
|
|
if ($this->transactors !== null) {
|
|
foreach ($this->transactors as $transactor) {
|
|
$transactor->rollback();
|
|
}
|
|
}
|
|
}
|
|
|
|
function _get(string $query, bool $entireRow=false) {
|
|
return $this->db()->querySingle($query, $entireRow);
|
|
}
|
|
|
|
function get($query, ?array $params=null, bool $entireRow=false) {
|
|
$db = $this->db();
|
|
$query = new query($query, $params);
|
|
if ($query->useStmt($db, $stmt, $sql)) {
|
|
try {
|
|
$result = $this->checkResult($stmt->execute());
|
|
try {
|
|
$row = $result->fetchArray(SQLITE3_ASSOC);
|
|
if ($row === false) return null;
|
|
$this->verifixRow($row);
|
|
if ($entireRow) return $row;
|
|
else return cl::first($row);
|
|
} finally {
|
|
$result->finalize();
|
|
}
|
|
} finally {
|
|
$stmt->close();
|
|
}
|
|
} else {
|
|
return $db->querySingle($sql, $entireRow);
|
|
}
|
|
}
|
|
|
|
function one($query, ?array $params=null): ?array {
|
|
return $this->get($query, $params, true);
|
|
}
|
|
|
|
protected function _fetchResult(SQLite3Result $result, ?SQLite3Stmt $stmt=null, $primaryKeys=null): Generator {
|
|
if ($primaryKeys !== null) $primaryKeys = cl::with($primaryKeys);
|
|
try {
|
|
while (($row = $result->fetchArray(SQLITE3_ASSOC)) !== false) {
|
|
$this->verifixRow($row);
|
|
if ($primaryKeys !== null) {
|
|
$key = implode("-", cl::select($row, $primaryKeys));
|
|
yield $key => $row;
|
|
} else {
|
|
yield $row;
|
|
}
|
|
}
|
|
} finally {
|
|
$result->finalize();
|
|
if ($stmt !== null) $stmt->close();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* si $primaryKeys est fourni, le résultat est indexé sur la(es) colonne(s)
|
|
* spécifiée(s)
|
|
*/
|
|
function all($query, ?array $params=null, $primaryKeys=null): iterable {
|
|
$db = $this->db();
|
|
$query = new query($query, $params);
|
|
if ($query->useStmt($db, $stmt, $sql)) {
|
|
$result = $this->checkResult($stmt->execute());
|
|
return $this->_fetchResult($result, $stmt, $primaryKeys);
|
|
} else {
|
|
$result = $this->checkResult($db->query($sql));
|
|
return $this->_fetchResult($result, null, $primaryKeys);
|
|
}
|
|
}
|
|
}
|