SQLite support for virtual columns
This commit is contained in:
parent
5e9b8b0078
commit
7b125b4d92
17 changed files with 371 additions and 482 deletions
|
@ -18,9 +18,6 @@
|
||||||
"test": [
|
"test": [
|
||||||
"phpunit"
|
"phpunit"
|
||||||
],
|
],
|
||||||
"test-local": [
|
|
||||||
"phpunit --testsuite Local"
|
|
||||||
],
|
|
||||||
"test-mysql": [
|
"test-mysql": [
|
||||||
"phpunit --testsuite MySQL"
|
"phpunit --testsuite MySQL"
|
||||||
],
|
],
|
||||||
|
|
|
@ -37,8 +37,12 @@ into the given table.
|
||||||
Search by random data field
|
Search by random data field
|
||||||
*/
|
*/
|
||||||
$search = $factory->search();
|
$search = $factory->search();
|
||||||
$search->where('${random_data} = :q');
|
$search->where('${random_data} LIKE :q');
|
||||||
$result = $search->execute(['q'=>'rw7nivub9bhhh3t4']);
|
$result = $search->execute(['q'=>'%ab%']);
|
||||||
|
foreach($result as $r) {
|
||||||
|
$r['random_data_2'] = md5(rand());
|
||||||
|
$r->update();
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Search by dso.id, which is much faster because it's indexed
|
Search by dso.id, which is much faster because it's indexed
|
||||||
|
|
|
@ -1,15 +1,10 @@
|
||||||
<phpunit bootstrap="vendor/autoload.php">
|
<phpunit bootstrap="vendor/autoload.php">
|
||||||
<testsuites>
|
<testsuites>
|
||||||
<testsuite name="Local">
|
|
||||||
<directory>tests</directory>
|
|
||||||
<exclude>tests/Drivers</exclude>
|
|
||||||
<exclude>tests/LegacyDrivers</exclude>
|
|
||||||
</testsuite>
|
|
||||||
<testsuite name="MySQL">
|
<testsuite name="MySQL">
|
||||||
<directory>tests/Drivers/MySQL</directory>
|
<directory>tests/Drivers/MySQL</directory>
|
||||||
</testsuite>
|
</testsuite>
|
||||||
<testsuite name="SQLite">
|
<testsuite name="SQLite">
|
||||||
<directory>tests/LegacyDrivers/SQLite</directory>
|
<directory>tests/Drivers/SQLite</directory>
|
||||||
</testsuite>
|
</testsuite>
|
||||||
</testsuites>
|
</testsuites>
|
||||||
</phpunit>
|
</phpunit>
|
||||||
|
|
|
@ -7,6 +7,7 @@ interface DSOFactoryInterface
|
||||||
public function __construct(Drivers\DSODriverInterface $driver, string $table);
|
public function __construct(Drivers\DSODriverInterface $driver, string $table);
|
||||||
|
|
||||||
public function class(array $data) : ?string;
|
public function class(array $data) : ?string;
|
||||||
|
public function virtualColumns(): array;
|
||||||
|
|
||||||
public function createTable() : bool;
|
public function createTable() : bool;
|
||||||
public function create(array $data = array()) : DSOInterface;
|
public function create(array $data = array()) : DSOInterface;
|
||||||
|
|
|
@ -7,7 +7,7 @@ class DriverFactory
|
||||||
public static $map = [
|
public static $map = [
|
||||||
'mariadb' => Drivers\MariaDBDriver::class,
|
'mariadb' => Drivers\MariaDBDriver::class,
|
||||||
'mysql' => Drivers\MySQLDriver::class,
|
'mysql' => Drivers\MySQLDriver::class,
|
||||||
'sqlite' => LegacyDrivers\SQLiteDriver::class,
|
'sqlite' => Drivers\SQLiteDriver::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
public static function factory(string $dsn, string $username = null, string $password = null, array $options = null, string $type = null): ?Drivers\DSODriverInterface
|
public static function factory(string $dsn, string $username = null, string $password = null, array $options = null, string $type = null): ?Drivers\DSODriverInterface
|
||||||
|
|
|
@ -10,7 +10,14 @@ abstract class AbstractDriver implements DSODriverInterface
|
||||||
{
|
{
|
||||||
public $lastPreparationErrorOn;
|
public $lastPreparationErrorOn;
|
||||||
public $pdo;
|
public $pdo;
|
||||||
const EXTENSIBLE_VIRTUAL_COLUMNS = true;
|
|
||||||
|
abstract protected function sql_select(array $args): string;
|
||||||
|
abstract protected function sql_count(array $args): string;
|
||||||
|
abstract protected function sql_ddl(array $args = []): string;
|
||||||
|
abstract protected function expandPath(string $path): string;
|
||||||
|
abstract protected function sql_setJSON(array $args): string;
|
||||||
|
abstract protected function sql_insert(array $args): string;
|
||||||
|
abstract protected function sql_delete(array $args): string;
|
||||||
|
|
||||||
public function __construct(string $dsn = null, string $username = null, string $password = null, array $options = null)
|
public function __construct(string $dsn = null, string $username = null, string $password = null, array $options = null)
|
||||||
{
|
{
|
||||||
|
|
|
@ -7,14 +7,14 @@ namespace Destructr\Drivers;
|
||||||
*/
|
*/
|
||||||
class MariaDBDriver extends MySQLDriver
|
class MariaDBDriver extends MySQLDriver
|
||||||
{
|
{
|
||||||
protected function sql_ddl($args=array())
|
protected function sql_ddl(array $args = []): string
|
||||||
{
|
{
|
||||||
$out = [];
|
$out = [];
|
||||||
$out[] = "CREATE TABLE IF NOT EXISTS `{$args['table']}` (";
|
$out[] = "CREATE TABLE IF NOT EXISTS `{$args['table']}` (";
|
||||||
$lines = [];
|
$lines = [];
|
||||||
$lines[] = "`json_data` JSON DEFAULT NULL";
|
$lines[] = "`json_data` JSON DEFAULT NULL";
|
||||||
foreach ($args['virtualColumns'] as $path => $col) {
|
foreach ($args['virtualColumns'] as $path => $col) {
|
||||||
$line = "`{$col['name']}` {$col['type']} GENERATED ALWAYS AS (".$this->expandPath($path).")";
|
$line = "`{$col['name']}` {$col['type']} GENERATED ALWAYS AS (" . $this->expandPath($path) . ")";
|
||||||
if (@$col['primary']) {
|
if (@$col['primary']) {
|
||||||
$line .= ' PERSISTENT';
|
$line .= ' PERSISTENT';
|
||||||
} else {
|
} else {
|
||||||
|
@ -31,7 +31,7 @@ class MariaDBDriver extends MySQLDriver
|
||||||
$lines[] = "KEY `{$args['table']}_{$col['name']}_idx` (`{$col['name']}`) USING $as";
|
$lines[] = "KEY `{$args['table']}_{$col['name']}_idx` (`{$col['name']}`) USING $as";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$out[] = implode(','.PHP_EOL, $lines);
|
$out[] = implode(',' . PHP_EOL, $lines);
|
||||||
$out[] = ") ENGINE=InnoDB DEFAULT CHARSET=utf8;";
|
$out[] = ") ENGINE=InnoDB DEFAULT CHARSET=utf8;";
|
||||||
return implode(PHP_EOL, $out);
|
return implode(PHP_EOL, $out);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ class MySQLDriver extends AbstractDriver
|
||||||
* column names if there are virtual columns configured for them. That
|
* column names if there are virtual columns configured for them. That
|
||||||
* happens in the Factory before it gets here.
|
* happens in the Factory before it gets here.
|
||||||
*/
|
*/
|
||||||
protected function sql_select($args)
|
protected function sql_select(array $args): string
|
||||||
{
|
{
|
||||||
//extract query parts from Search and expand paths
|
//extract query parts from Search and expand paths
|
||||||
$where = $this->expandPaths($args['search']->where());
|
$where = $this->expandPaths($args['search']->where());
|
||||||
|
@ -24,25 +24,25 @@ class MySQLDriver extends AbstractDriver
|
||||||
$out = ["SELECT * FROM `{$args['table']}`"];
|
$out = ["SELECT * FROM `{$args['table']}`"];
|
||||||
//where statement
|
//where statement
|
||||||
if ($where !== null) {
|
if ($where !== null) {
|
||||||
$out[] = "WHERE ".$where;
|
$out[] = "WHERE " . $where;
|
||||||
}
|
}
|
||||||
//order statement
|
//order statement
|
||||||
if ($order !== null) {
|
if ($order !== null) {
|
||||||
$out[] = "ORDER BY ".$order;
|
$out[] = "ORDER BY " . $order;
|
||||||
}
|
}
|
||||||
//limit
|
//limit
|
||||||
if ($limit !== null) {
|
if ($limit !== null) {
|
||||||
$out[] = "LIMIT ".$limit;
|
$out[] = "LIMIT " . $limit;
|
||||||
}
|
}
|
||||||
//offset
|
//offset
|
||||||
if ($offset !== null) {
|
if ($offset !== null) {
|
||||||
$out[] = "OFFSET ".$offset;
|
$out[] = "OFFSET " . $offset;
|
||||||
}
|
}
|
||||||
//return
|
//return
|
||||||
return implode(PHP_EOL, $out).';';
|
return implode(PHP_EOL, $out) . ';';
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function sql_count($args)
|
protected function sql_count(array $args): string
|
||||||
{
|
{
|
||||||
//extract query parts from Search and expand paths
|
//extract query parts from Search and expand paths
|
||||||
$where = $this->expandPaths($args['search']->where());
|
$where = $this->expandPaths($args['search']->where());
|
||||||
|
@ -50,20 +50,20 @@ class MySQLDriver extends AbstractDriver
|
||||||
$out = ["SELECT count(dso_id) FROM `{$args['table']}`"];
|
$out = ["SELECT count(dso_id) FROM `{$args['table']}`"];
|
||||||
//where statement
|
//where statement
|
||||||
if ($where !== null) {
|
if ($where !== null) {
|
||||||
$out[] = "WHERE ".$where;
|
$out[] = "WHERE " . $where;
|
||||||
}
|
}
|
||||||
//return
|
//return
|
||||||
return implode(PHP_EOL, $out).';';
|
return implode(PHP_EOL, $out) . ';';
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function sql_ddl($args=array())
|
protected function sql_ddl(array $args = []): string
|
||||||
{
|
{
|
||||||
$out = [];
|
$out = [];
|
||||||
$out[] = "CREATE TABLE IF NOT EXISTS `{$args['table']}` (";
|
$out[] = "CREATE TABLE IF NOT EXISTS `{$args['table']}` (";
|
||||||
$lines = [];
|
$lines = [];
|
||||||
$lines[] = "`json_data` JSON DEFAULT NULL";
|
$lines[] = "`json_data` JSON DEFAULT NULL";
|
||||||
foreach ($args['virtualColumns'] as $path => $col) {
|
foreach ($args['virtualColumns'] as $path => $col) {
|
||||||
$line = "`{$col['name']}` {$col['type']} GENERATED ALWAYS AS (".$this->expandPath($path).")";
|
$line = "`{$col['name']}` {$col['type']} GENERATED ALWAYS AS (" . $this->expandPath($path) . ")";
|
||||||
if (@$col['primary']) {
|
if (@$col['primary']) {
|
||||||
$line .= ' STORED';
|
$line .= ' STORED';
|
||||||
} else {
|
} else {
|
||||||
|
@ -80,28 +80,28 @@ class MySQLDriver extends AbstractDriver
|
||||||
$lines[] = "KEY `{$args['table']}_{$col['name']}_idx` (`{$col['name']}`) USING $as";
|
$lines[] = "KEY `{$args['table']}_{$col['name']}_idx` (`{$col['name']}`) USING $as";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$out[] = implode(','.PHP_EOL, $lines);
|
$out[] = implode(',' . PHP_EOL, $lines);
|
||||||
$out[] = ") ENGINE=InnoDB DEFAULT CHARSET=utf8;";
|
$out[] = ") ENGINE=InnoDB DEFAULT CHARSET=utf8;";
|
||||||
return implode(PHP_EOL, $out);
|
return implode(PHP_EOL, $out);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function expandPath(string $path) : string
|
protected function expandPath(string $path): string
|
||||||
{
|
{
|
||||||
return "JSON_UNQUOTE(JSON_EXTRACT(`json_data`,'$.{$path}'))";
|
return "JSON_UNQUOTE(JSON_EXTRACT(`json_data`,'$.{$path}'))";
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function sql_setJSON($args)
|
protected function sql_setJSON(array $args): string
|
||||||
{
|
{
|
||||||
return 'UPDATE `'.$args['table'].'` SET `json_data` = :data WHERE `dso_id` = :dso_id;';
|
return 'UPDATE `' . $args['table'] . '` SET `json_data` = :data WHERE `dso_id` = :dso_id;';
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function sql_insert($args)
|
protected function sql_insert(array $args): string
|
||||||
{
|
{
|
||||||
return "INSERT INTO `{$args['table']}` (`json_data`) VALUES (:data);";
|
return "INSERT INTO `{$args['table']}` (`json_data`) VALUES (:data);";
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function sql_delete($args)
|
protected function sql_delete(array $args): string
|
||||||
{
|
{
|
||||||
return 'DELETE FROM `'.$args['table'].'` WHERE `dso_id` = :dso_id;';
|
return 'DELETE FROM `' . $args['table'] . '` WHERE `dso_id` = :dso_id;';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
260
src/Drivers/SQLiteDriver.php
Normal file
260
src/Drivers/SQLiteDriver.php
Normal file
|
@ -0,0 +1,260 @@
|
||||||
|
<?php
|
||||||
|
/* Destructr | https://github.com/jobyone/destructr | MIT License */
|
||||||
|
namespace Destructr\Drivers;
|
||||||
|
|
||||||
|
use Destructr\DSOInterface;
|
||||||
|
use Destructr\Factory;
|
||||||
|
use Destructr\Search;
|
||||||
|
use Flatrr\FlatArray;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What this driver supports: Any version of SQLite3 in PHP environments that allow
|
||||||
|
* pdo::sqliteCreateFunction
|
||||||
|
*
|
||||||
|
* Note that unlike databases with native JSON functions, this driver's generated
|
||||||
|
* columns are NOT generated in the database. They are updated by this class whenever
|
||||||
|
* the data they reference changes. This doesn't matter much if you're doing all
|
||||||
|
* your updating through Destructr, but is something to be cognizent of if your
|
||||||
|
* data is being updated outside Destructr.
|
||||||
|
*/
|
||||||
|
class SQLiteDriver extends AbstractDriver
|
||||||
|
{
|
||||||
|
public function select(string $table, Search $search, array $params)
|
||||||
|
{
|
||||||
|
$results = parent::select($table, $search, $params);
|
||||||
|
foreach ($results as $rkey => $row) {
|
||||||
|
$new = new FlatArray();
|
||||||
|
foreach (json_decode($row['json_data'], true) as $key => $value) {
|
||||||
|
$new->set(str_replace('|', '.', $key), $value);
|
||||||
|
}
|
||||||
|
$results[$rkey]['json_data'] = json_encode($new->get());
|
||||||
|
}
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function sql_count(array $args): string
|
||||||
|
{
|
||||||
|
//extract query parts from Search and expand paths
|
||||||
|
$where = $this->expandPaths($args['search']->where());
|
||||||
|
//select from
|
||||||
|
$out = ["SELECT count(dso_id) FROM `{$args['table']}`"];
|
||||||
|
//where statement
|
||||||
|
if ($where !== null) {
|
||||||
|
$out[] = "WHERE " . $where;
|
||||||
|
}
|
||||||
|
//return
|
||||||
|
return implode(PHP_EOL, $out) . ';';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function sql_select(array $args): string
|
||||||
|
{
|
||||||
|
//extract query parts from Search and expand paths
|
||||||
|
$where = $this->expandPaths($args['search']->where());
|
||||||
|
$order = $this->expandPaths($args['search']->order());
|
||||||
|
$limit = $args['search']->limit();
|
||||||
|
$offset = $args['search']->offset();
|
||||||
|
//select from
|
||||||
|
$out = ["SELECT * FROM `{$args['table']}`"];
|
||||||
|
//where statement
|
||||||
|
if ($where !== null) {
|
||||||
|
$out[] = "WHERE " . $where;
|
||||||
|
}
|
||||||
|
//order statement
|
||||||
|
if ($order !== null) {
|
||||||
|
$out[] = "ORDER BY " . $order;
|
||||||
|
}
|
||||||
|
//limit
|
||||||
|
if ($limit !== null) {
|
||||||
|
$out[] = "LIMIT " . $limit;
|
||||||
|
}
|
||||||
|
//offset
|
||||||
|
if ($offset !== null) {
|
||||||
|
$out[] = "OFFSET " . $offset;
|
||||||
|
}
|
||||||
|
//return
|
||||||
|
return implode(PHP_EOL, $out) . ';';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(string $table, DSOInterface $dso): bool
|
||||||
|
{
|
||||||
|
if (!$dso->changes() && !$dso->removals()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
$columns = $this->dso_columns($dso);
|
||||||
|
$s = $this->getStatement(
|
||||||
|
'setJSON',
|
||||||
|
[
|
||||||
|
'table' => $table,
|
||||||
|
'columns' => $columns,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
return $s->execute($columns);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function insert(string $table, DSOInterface $dso): bool
|
||||||
|
{
|
||||||
|
$columns = $this->dso_columns($dso);
|
||||||
|
$s = $this->getStatement(
|
||||||
|
'insert',
|
||||||
|
[
|
||||||
|
'table' => $table,
|
||||||
|
'columns' => $columns,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
return $s->execute($columns);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function sql_insert(array $args): string
|
||||||
|
{
|
||||||
|
$out = [];
|
||||||
|
$names = array_map(
|
||||||
|
function ($e) {
|
||||||
|
return preg_replace('/^:/', '', $e);
|
||||||
|
},
|
||||||
|
array_keys($args['columns'])
|
||||||
|
);
|
||||||
|
$out[] = 'INSERT INTO `' . $args['table'] . '`';
|
||||||
|
$out[] = '(`' . implode('`,`', $names) . '`)';
|
||||||
|
$out[] = 'VALUES (:' . implode(',:', $names) . ')';
|
||||||
|
$out = implode(PHP_EOL, $out) . ';';
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function sql_setJSON(array $args): string
|
||||||
|
{
|
||||||
|
$names = array_map(
|
||||||
|
function ($e) {
|
||||||
|
return '`'.preg_replace('/^:/', '', $e).'` = '.$e;
|
||||||
|
},
|
||||||
|
array_keys($args['columns'])
|
||||||
|
);
|
||||||
|
$out = [];
|
||||||
|
$out[] = 'UPDATE `' . $args['table'] . '`';
|
||||||
|
$out[] = 'SET';
|
||||||
|
$out[] = implode(','.PHP_EOL,$names);
|
||||||
|
$out[] = 'WHERE `dso_id` = :dso_id';
|
||||||
|
$out = implode(PHP_EOL, $out) . ';';
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(string $table, DSOInterface $dso): bool
|
||||||
|
{
|
||||||
|
$s = $this->getStatement(
|
||||||
|
'delete',
|
||||||
|
['table' => $table]
|
||||||
|
);
|
||||||
|
return $s->execute([
|
||||||
|
':dso_id' => $dso['dso.id'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function sql_delete(array $args): string
|
||||||
|
{
|
||||||
|
return 'DELETE FROM `' . $args['table'] . '` WHERE `dso_id` = :dso_id;';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to extract a list of column/parameter names for a given DSO, based
|
||||||
|
* on the current values.
|
||||||
|
*
|
||||||
|
* @param DSOInterface $dso
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function dso_columns(DSOInterface $dso)
|
||||||
|
{
|
||||||
|
$columns = [':json_data' => $this->json_encode($dso->get())];
|
||||||
|
foreach ($dso->factory()->virtualColumns() as $vk => $vv) {
|
||||||
|
$columns[':' . $vv['name']] = $dso->get($vk);
|
||||||
|
}
|
||||||
|
return $columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intercept calls to set PDO, and add a custom function to SQLite so that it
|
||||||
|
* can extract JSON values. It's not actually terribly slow, and allows us to
|
||||||
|
* use JSON seamlessly, almost as if it were native.
|
||||||
|
*
|
||||||
|
* @param \PDO $pdo
|
||||||
|
* @return \PDO|null
|
||||||
|
*/
|
||||||
|
public function pdo(\PDO $pdo = null): ?\PDO
|
||||||
|
{
|
||||||
|
if ($pdo) {
|
||||||
|
$this->pdo = $pdo;
|
||||||
|
$this->pdo->sqliteCreateFunction(
|
||||||
|
'DESTRUCTR_JSON_EXTRACT',
|
||||||
|
'\\Destructr\\Drivers\\SQLiteDriver::JSON_EXTRACT',
|
||||||
|
2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return $this->pdo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function JSON_EXTRACT($json, $path)
|
||||||
|
{
|
||||||
|
$path = substr($path, 2);
|
||||||
|
$path = explode('.', $path);
|
||||||
|
$arr = json_decode($json, true);
|
||||||
|
$out = &$arr;
|
||||||
|
while ($key = array_shift($path)) {
|
||||||
|
if (isset($out[$key])) {
|
||||||
|
$out = &$out[$key];
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return @"$out";
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createTable(string $table, array $virtualColumns): bool
|
||||||
|
{
|
||||||
|
$sql = $this->sql_ddl([
|
||||||
|
'table' => $table,
|
||||||
|
'virtualColumns' => $virtualColumns,
|
||||||
|
]);
|
||||||
|
$out = $this->pdo->exec($sql) !== false;
|
||||||
|
foreach ($virtualColumns as $key => $vcol) {
|
||||||
|
$idxResult = true;
|
||||||
|
if (@$vcol['primary']) {
|
||||||
|
//sqlite automatically creates this index
|
||||||
|
} elseif (@$vcol['unique']) {
|
||||||
|
$idxResult = $this->pdo->exec('CREATE UNIQUE INDEX ' . $table . '_' . $vcol['name'] . '_idx on `' . $table . '`(`' . $vcol['name'] . '`)') !== false;
|
||||||
|
} elseif (@$vcol['index']) {
|
||||||
|
$idxResult = $this->pdo->exec('CREATE INDEX ' . $table . '_' . $vcol['name'] . '_idx on `' . $table . '`(`' . $vcol['name'] . '`)') !== false;
|
||||||
|
}
|
||||||
|
if (!$idxResult) {
|
||||||
|
$out = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function sql_ddl(array $args = []): string
|
||||||
|
{
|
||||||
|
$out = [];
|
||||||
|
$out[] = "CREATE TABLE IF NOT EXISTS `{$args['table']}` (";
|
||||||
|
$lines = [];
|
||||||
|
$lines[] = "`json_data` TEXT DEFAULT NULL";
|
||||||
|
foreach ($args['virtualColumns'] as $path => $col) {
|
||||||
|
$line = "`{$col['name']}` {$col['type']}";
|
||||||
|
if (@$col['primary']) {
|
||||||
|
$line .= ' PRIMARY KEY';
|
||||||
|
}
|
||||||
|
$lines[] = $line;
|
||||||
|
}
|
||||||
|
$out[] = implode(',' . PHP_EOL, $lines);
|
||||||
|
$out[] = ");";
|
||||||
|
$out = implode(PHP_EOL, $out);
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function expandPath(string $path): string
|
||||||
|
{
|
||||||
|
return "DESTRUCTR_JSON_EXTRACT(`json_data`,'$.{$path}')";
|
||||||
|
}
|
||||||
|
|
||||||
|
public function json_encode($a, ?array &$b = null, string $prefix = '')
|
||||||
|
{
|
||||||
|
return json_encode($a);
|
||||||
|
}
|
||||||
|
}
|
|
@ -33,44 +33,22 @@ class Factory implements DSOFactoryInterface
|
||||||
*/
|
*/
|
||||||
protected $virtualColumns = [
|
protected $virtualColumns = [
|
||||||
'dso.id' => [
|
'dso.id' => [
|
||||||
'name'=>'dso_id',
|
'name' => 'dso_id',
|
||||||
'type'=>'VARCHAR(16)',
|
'type' => 'VARCHAR(16)',
|
||||||
'index' => 'BTREE',
|
'index' => 'BTREE',
|
||||||
'unique' => true,
|
'unique' => true,
|
||||||
'primary' => true
|
'primary' => true,
|
||||||
],
|
],
|
||||||
'dso.type' => [
|
'dso.type' => [
|
||||||
'name'=>'dso_type',
|
'name' => 'dso_type',
|
||||||
'type'=>'VARCHAR(30)',
|
'type' => 'VARCHAR(30)',
|
||||||
'index'=>'BTREE'
|
|
||||||
],
|
|
||||||
'dso.deleted' => [
|
|
||||||
'name'=>'dso_deleted',
|
|
||||||
'type'=>'BIGINT',
|
|
||||||
'index'=>'BTREE'
|
|
||||||
]
|
|
||||||
];
|
|
||||||
/**
|
|
||||||
* This cannot be modified by extending classes, it's used by legacy drivers
|
|
||||||
*/
|
|
||||||
const CORE_VIRTUAL_COLUMNS = [
|
|
||||||
'dso.id' => [
|
|
||||||
'name'=>'dso_id',
|
|
||||||
'type'=>'VARCHAR(16)',
|
|
||||||
'index' => 'BTREE',
|
'index' => 'BTREE',
|
||||||
'unique' => true,
|
|
||||||
'primary' => true
|
|
||||||
],
|
|
||||||
'dso.type' => [
|
|
||||||
'name'=>'dso_type',
|
|
||||||
'type'=>'VARCHAR(30)',
|
|
||||||
'index'=>'BTREE'
|
|
||||||
],
|
],
|
||||||
'dso.deleted' => [
|
'dso.deleted' => [
|
||||||
'name'=>'dso_deleted',
|
'name' => 'dso_deleted',
|
||||||
'type'=>'BIGINT',
|
'type' => 'BIGINT',
|
||||||
'index'=>'BTREE'
|
'index' => 'BTREE',
|
||||||
]
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
public function __construct(Drivers\DSODriverInterface $driver, string $table)
|
public function __construct(Drivers\DSODriverInterface $driver, string $table)
|
||||||
|
@ -79,7 +57,7 @@ class Factory implements DSOFactoryInterface
|
||||||
$this->table = $table;
|
$this->table = $table;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function quote(string $str) : string
|
public function quote(string $str): string
|
||||||
{
|
{
|
||||||
return $this->driver->pdo()->quote($str);
|
return $this->driver->pdo()->quote($str);
|
||||||
}
|
}
|
||||||
|
@ -93,14 +71,14 @@ class Factory implements DSOFactoryInterface
|
||||||
$dso->set('dso.created.date', time());
|
$dso->set('dso.created.date', time());
|
||||||
}
|
}
|
||||||
if (!$dso->get('dso.created.user')) {
|
if (!$dso->get('dso.created.user')) {
|
||||||
$dso->set('dso.created.user', ['ip'=>@$_SERVER['REMOTE_ADDR']]);
|
$dso->set('dso.created.user', ['ip' => @$_SERVER['REMOTE_ADDR']]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function hook_update(DSOInterface $dso)
|
protected function hook_update(DSOInterface $dso)
|
||||||
{
|
{
|
||||||
$dso->set('dso.modified.date', time());
|
$dso->set('dso.modified.date', time());
|
||||||
$dso->set('dso.modified.user', ['ip'=>@$_SERVER['REMOTE_ADDR']]);
|
$dso->set('dso.modified.user', ['ip' => @$_SERVER['REMOTE_ADDR']]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -112,12 +90,12 @@ class Factory implements DSOFactoryInterface
|
||||||
* @param array $data
|
* @param array $data
|
||||||
* @return string|null
|
* @return string|null
|
||||||
*/
|
*/
|
||||||
public function class(array $data) : ?string
|
function class (array $data): ?string
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(DSOInterface $dso, bool $permanent = false) : bool
|
public function delete(DSOInterface $dso, bool $permanent = false): bool
|
||||||
{
|
{
|
||||||
if ($permanent) {
|
if ($permanent) {
|
||||||
return $this->driver->delete($this->table, $dso);
|
return $this->driver->delete($this->table, $dso);
|
||||||
|
@ -126,13 +104,13 @@ class Factory implements DSOFactoryInterface
|
||||||
return $this->update($dso, true);
|
return $this->update($dso, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function undelete(DSOInterface $dso) : bool
|
public function undelete(DSOInterface $dso): bool
|
||||||
{
|
{
|
||||||
unset($dso['dso.deleted']);
|
unset($dso['dso.deleted']);
|
||||||
return $this->update($dso, true);
|
return $this->update($dso, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create(array $data = array()) : DSOInterface
|
public function create(array $data = array()): DSOInterface
|
||||||
{
|
{
|
||||||
if (!($class = $this->class($data))) {
|
if (!($class = $this->class($data))) {
|
||||||
$class = DSO::class;
|
$class = DSO::class;
|
||||||
|
@ -144,25 +122,25 @@ class Factory implements DSOFactoryInterface
|
||||||
return $dso;
|
return $dso;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function createTable() : bool
|
public function createTable(): bool
|
||||||
{
|
{
|
||||||
return $this->driver->createTable(
|
return $this->driver->createTable(
|
||||||
$this->table,
|
$this->table,
|
||||||
($this->driver::EXTENSIBLE_VIRTUAL_COLUMNS?$this->virtualColumns:$this::CORE_VIRTUAL_COLUMNS)
|
$this->virtualColumns
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function virtualColumnName($path) : ?string
|
public function virtualColumns(): array
|
||||||
{
|
{
|
||||||
if ($this->driver::EXTENSIBLE_VIRTUAL_COLUMNS) {
|
return $this->virtualColumns;
|
||||||
$vcols = $this->virtualColumns;
|
|
||||||
} else {
|
|
||||||
$vcols = static::CORE_VIRTUAL_COLUMNS;
|
|
||||||
}
|
|
||||||
return @$vcols[$path]['name'];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update(DSOInterface $dso, bool $sneaky = false) : bool
|
protected function virtualColumnName($path): ?string
|
||||||
|
{
|
||||||
|
return @$this->virtualColumns[$path]['name'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(DSOInterface $dso, bool $sneaky = false): bool
|
||||||
{
|
{
|
||||||
if (!$dso->changes() && !$dso->removals()) {
|
if (!$dso->changes() && !$dso->removals()) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -176,7 +154,7 @@ class Factory implements DSOFactoryInterface
|
||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function search() : Search
|
public function search(): Search
|
||||||
{
|
{
|
||||||
return new Search($this);
|
return new Search($this);
|
||||||
}
|
}
|
||||||
|
@ -195,7 +173,7 @@ class Factory implements DSOFactoryInterface
|
||||||
return $arr;
|
return $arr;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function executeCount(Search $search, array $params = array(), $deleted = false) : ?int
|
public function executeCount(Search $search, array $params = array(), $deleted = false): ?int
|
||||||
{
|
{
|
||||||
//add deletion clause and expand column names
|
//add deletion clause and expand column names
|
||||||
$search = $this->preprocessSearch($search, $deleted);
|
$search = $this->preprocessSearch($search, $deleted);
|
||||||
|
@ -207,7 +185,7 @@ class Factory implements DSOFactoryInterface
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function executeSearch(Search $search, array $params = array(), $deleted = false) : array
|
public function executeSearch(Search $search, array $params = array(), $deleted = false): array
|
||||||
{
|
{
|
||||||
//add deletion clause and expand column names
|
//add deletion clause and expand column names
|
||||||
$search = $this->preprocessSearch($search, $deleted);
|
$search = $this->preprocessSearch($search, $deleted);
|
||||||
|
@ -220,17 +198,17 @@ class Factory implements DSOFactoryInterface
|
||||||
return $this->makeObjectsFromRows($r);
|
return $this->makeObjectsFromRows($r);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function read(string $value, string $field = 'dso.id', $deleted = false) : ?DSOInterface
|
public function read(string $value, string $field = 'dso.id', $deleted = false): ?DSOInterface
|
||||||
{
|
{
|
||||||
$search = $this->search();
|
$search = $this->search();
|
||||||
$search->where('${'.$field.'} = :value');
|
$search->where('${' . $field . '} = :value');
|
||||||
if ($results = $search->execute([':value'=>$value], $deleted)) {
|
if ($results = $search->execute([':value' => $value], $deleted)) {
|
||||||
return array_shift($results);
|
return array_shift($results);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function insert(DSOInterface $dso) : bool
|
public function insert(DSOInterface $dso): bool
|
||||||
{
|
{
|
||||||
$this->hook_update($dso);
|
$this->hook_update($dso);
|
||||||
$dso->hook_update();
|
$dso->hook_update();
|
||||||
|
@ -255,14 +233,14 @@ class Factory implements DSOFactoryInterface
|
||||||
$added = '${dso.deleted} is null';
|
$added = '${dso.deleted} is null';
|
||||||
}
|
}
|
||||||
if ($where) {
|
if ($where) {
|
||||||
$where = '('.$where.') AND '.$added;
|
$where = '(' . $where . ') AND ' . $added;
|
||||||
} else {
|
} else {
|
||||||
$where = $added;
|
$where = $added;
|
||||||
}
|
}
|
||||||
$search->where($where);
|
$search->where($where);
|
||||||
}
|
}
|
||||||
/* expand virtual column names */
|
/* expand virtual column names */
|
||||||
foreach (['where','order'] as $clause) {
|
foreach (['where', 'order'] as $clause) {
|
||||||
if ($value = $search->$clause()) {
|
if ($value = $search->$clause()) {
|
||||||
$value = preg_replace_callback(
|
$value = preg_replace_callback(
|
||||||
'/\$\{([^\}\\\]+)\}/',
|
'/\$\{([^\}\\\]+)\}/',
|
||||||
|
@ -282,7 +260,7 @@ class Factory implements DSOFactoryInterface
|
||||||
return $search;
|
return $search;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static function generate_id($chars, $length) : string
|
protected static function generate_id($chars, $length): string
|
||||||
{
|
{
|
||||||
$check = new Check();
|
$check = new Check();
|
||||||
do {
|
do {
|
||||||
|
@ -290,7 +268,7 @@ class Factory implements DSOFactoryInterface
|
||||||
while (strlen($id) < $length) {
|
while (strlen($id) < $length) {
|
||||||
$id .= substr(
|
$id .= substr(
|
||||||
$chars,
|
$chars,
|
||||||
rand(0, strlen($chars)-1),
|
rand(0, strlen($chars) - 1),
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,197 +0,0 @@
|
||||||
<?php
|
|
||||||
/* Destructr | https://github.com/jobyone/destructr | MIT License */
|
|
||||||
namespace Destructr\LegacyDrivers;
|
|
||||||
|
|
||||||
use Destructr\Drivers\AbstractDriver;
|
|
||||||
use Destructr\DSOInterface;
|
|
||||||
use Destructr\Factory;
|
|
||||||
use Destructr\Search;
|
|
||||||
use Flatrr\FlatArray;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This driver is for supporting older SQL servers that don't have their own
|
|
||||||
* JSON functions. It uses a highly suspect alternative JSON serialization and
|
|
||||||
* user-defined function.
|
|
||||||
*
|
|
||||||
* There are also DEFINITELY bugs in legacy drivers. They should only be used
|
|
||||||
* for very simple queries. These are probably also bugs that cannot be fixed.
|
|
||||||
* Legacy drivers shouldn't really be considered "supported" per se.
|
|
||||||
*/
|
|
||||||
class AbstractLegacyDriver extends AbstractDriver
|
|
||||||
{
|
|
||||||
const EXTENSIBLE_VIRTUAL_COLUMNS = false;
|
|
||||||
|
|
||||||
public function select(string $table, Search $search, array $params)
|
|
||||||
{
|
|
||||||
$results = parent::select($table, $search, $params);
|
|
||||||
foreach ($results as $rkey => $row) {
|
|
||||||
$new = new FlatArray();
|
|
||||||
foreach (json_decode($row['json_data'], true) as $key => $value) {
|
|
||||||
$new->set(str_replace('|', '.', $key), $value);
|
|
||||||
}
|
|
||||||
$results[$rkey]['json_data'] = json_encode($new->get());
|
|
||||||
}
|
|
||||||
return $results;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function sql_count($args)
|
|
||||||
{
|
|
||||||
//extract query parts from Search and expand paths
|
|
||||||
$where = $this->expandPaths($args['search']->where());
|
|
||||||
//select from
|
|
||||||
$out = ["SELECT count(dso_id) FROM `{$args['table']}`"];
|
|
||||||
//where statement
|
|
||||||
if ($where !== null) {
|
|
||||||
$out[] = "WHERE ".$where;
|
|
||||||
}
|
|
||||||
//return
|
|
||||||
return implode(PHP_EOL, $out).';';
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function sql_select($args)
|
|
||||||
{
|
|
||||||
//extract query parts from Search and expand paths
|
|
||||||
$where = $this->expandPaths($args['search']->where());
|
|
||||||
$order = $this->expandPaths($args['search']->order());
|
|
||||||
$limit = $args['search']->limit();
|
|
||||||
$offset = $args['search']->offset();
|
|
||||||
//select from
|
|
||||||
$out = ["SELECT * FROM `{$args['table']}`"];
|
|
||||||
//where statement
|
|
||||||
if ($where !== null) {
|
|
||||||
$out[] = "WHERE ".$where;
|
|
||||||
}
|
|
||||||
//order statement
|
|
||||||
if ($order !== null) {
|
|
||||||
$out[] = "ORDER BY ".$order;
|
|
||||||
}
|
|
||||||
//limit
|
|
||||||
if ($limit !== null) {
|
|
||||||
$out[] = "LIMIT ".$limit;
|
|
||||||
}
|
|
||||||
//offset
|
|
||||||
if ($offset !== null) {
|
|
||||||
$out[] = "OFFSET ".$offset;
|
|
||||||
}
|
|
||||||
//return
|
|
||||||
return implode(PHP_EOL, $out).';';
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function sql_ddl($args=array())
|
|
||||||
{
|
|
||||||
$out = [];
|
|
||||||
$out[] = "CREATE TABLE `{$args['table']}` (";
|
|
||||||
$lines = [];
|
|
||||||
$lines[] = "`json_data` TEXT DEFAULT NULL";
|
|
||||||
foreach ($args['virtualColumns'] as $path => $col) {
|
|
||||||
$lines[] = "`{$col['name']}` {$col['type']}";
|
|
||||||
}
|
|
||||||
foreach ($args['virtualColumns'] as $path => $col) {
|
|
||||||
if (@$col['unique'] && $as = @$col['index']) {
|
|
||||||
$lines[] = "UNIQUE KEY `{$args['table']}_{$col['name']}_idx` (`{$col['name']}`) USING $as";
|
|
||||||
} elseif ($as = @$col['index']) {
|
|
||||||
$lines[] = "KEY `{$args['table']}_{$col['name']}_idx` (`{$col['name']}`) USING $as";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$out[] = implode(','.PHP_EOL, $lines);
|
|
||||||
$out[] = ") ENGINE=InnoDB DEFAULT CHARSET=utf8;";
|
|
||||||
return implode(PHP_EOL, $out);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function update(string $table, DSOInterface $dso) : bool
|
|
||||||
{
|
|
||||||
if (!$dso->changes() && !$dso->removals()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
$s = $this->getStatement(
|
|
||||||
'setJSON',
|
|
||||||
['table'=>$table]
|
|
||||||
);
|
|
||||||
$params = $this->legacyParams($dso);
|
|
||||||
$out = $s->execute($params);
|
|
||||||
return $out;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function sql_setJSON($args)
|
|
||||||
{
|
|
||||||
$out = [];
|
|
||||||
$out[] = 'UPDATE `'.$args['table'].'`';
|
|
||||||
$out[] = 'SET';
|
|
||||||
foreach (Factory::CORE_VIRTUAL_COLUMNS as $v) {
|
|
||||||
$out[] = '`'.$v['name'].'` = :'.$v['name'].',';
|
|
||||||
}
|
|
||||||
$out[] = '`json_data` = :data';
|
|
||||||
$out[] = 'WHERE `dso_id` = :dso_id';
|
|
||||||
return implode(PHP_EOL, $out).';';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function insert(string $table, DSOInterface $dso) : bool
|
|
||||||
{
|
|
||||||
$s = $this->getStatement(
|
|
||||||
'insert',
|
|
||||||
['table'=>$table]
|
|
||||||
);
|
|
||||||
$params = $this->legacyParams($dso);
|
|
||||||
return $s->execute($params);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function legacyParams(DSOInterface $dso)
|
|
||||||
{
|
|
||||||
$params = [':data' => $this->json_encode($dso->get())];
|
|
||||||
foreach (Factory::CORE_VIRTUAL_COLUMNS as $vk => $vv) {
|
|
||||||
$params[':'.$vv['name']] = $dso->get($vk);
|
|
||||||
}
|
|
||||||
return $params;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function sql_insert($args)
|
|
||||||
{
|
|
||||||
$out = [];
|
|
||||||
$out[] = 'INSERT INTO `'.$args['table'].'`';
|
|
||||||
$out[] = '(`json_data`,`dso_id`,`dso_type`,`dso_deleted`)';
|
|
||||||
$out[] = 'VALUES (:data, :dso_id, :dso_type, :dso_deleted)';
|
|
||||||
return implode(PHP_EOL, $out).';';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function delete(string $table, DSOInterface $dso) : bool
|
|
||||||
{
|
|
||||||
$s = $this->getStatement(
|
|
||||||
'delete',
|
|
||||||
['table'=>$table]
|
|
||||||
);
|
|
||||||
$out = $s->execute([
|
|
||||||
':dso_id' => $dso['dso.id']
|
|
||||||
]);
|
|
||||||
// if (!$out) {
|
|
||||||
// var_dump($s->errorInfo());
|
|
||||||
// }
|
|
||||||
return $out;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function sql_delete($args)
|
|
||||||
{
|
|
||||||
return 'DELETE FROM `'.$args['table'].'` WHERE `dso_id` = :dso_id;';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function json_encode($a, array &$b = null, string $prefix = '')
|
|
||||||
{
|
|
||||||
if ($b === null) {
|
|
||||||
$b = [];
|
|
||||||
$this->json_encode($a, $b, '');
|
|
||||||
return json_encode($b);
|
|
||||||
} else {
|
|
||||||
if (is_array($a)) {
|
|
||||||
foreach ($a as $ak => $av) {
|
|
||||||
if ($prefix == '') {
|
|
||||||
$nprefix = $ak;
|
|
||||||
} else {
|
|
||||||
$nprefix = $prefix.'|'.$ak;
|
|
||||||
}
|
|
||||||
$this->json_encode($av, $b, $nprefix);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$b[$prefix] = $a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
# Destructr legacy drivers
|
|
||||||
|
|
||||||
This folder holds what are called "legacy drivers."
|
|
||||||
These drivers attempt to extend Destructr support to as many databases as possible,
|
|
||||||
even in some cases at the expense of functionality.
|
|
||||||
|
|
||||||
All these drivers are designed to support the following features:
|
|
||||||
* Inserting objects (and disallowing duplicates at the *SQL* level)
|
|
||||||
* Updating existing objects
|
|
||||||
* Deleting objects
|
|
||||||
* Searching by exact text matches
|
|
||||||
|
|
||||||
The things many of this sort of driver will *not* ever support:
|
|
||||||
* Performance optimization via virtual columns. Many of them will have their
|
|
||||||
performance lightly optimized for common system queries, just by hard-coding
|
|
||||||
virtual columns for `dso.id`, `dso.deleted`, and `dso.type`
|
|
||||||
* Complex queries, like joins, REGEX, or LIKE queries.
|
|
||||||
|
|
||||||
## Current state of legacy drivers
|
|
||||||
|
|
||||||
### SQLite 3
|
|
||||||
|
|
||||||
**\Destructr\LegacyDrivers\SQLiteDriver**
|
|
||||||
|
|
||||||
**Overall support level: Highly functional, a touch slow**
|
|
||||||
|
|
||||||
Via LegacyDrivers\SQLiteDriver Destructr now has surprisingly good support for
|
|
||||||
SQLite 3. It accomplishes this by using SQLite's user-defined functions feature
|
|
||||||
to offload JSON parsing to PHP. This means all the JSON parsing is actually up
|
|
||||||
to spec, storage doesn't need to be flattened like the other legacy drivers,
|
|
||||||
and tables are dramatically more easily forward-compatible.
|
|
||||||
|
|
||||||
If you're considering using legacy MySQL, you should really seriously consider
|
|
||||||
just using SQLite instead. In benchmarks with 1000 records SQLite's performance
|
|
||||||
is actually BETTER than MySQL 5.6 for everything but insert/update operations.
|
|
||||||
In some cases it appears to even be *significantly* faster while also having the
|
|
||||||
distinct advantage of not using any goofy home-rolled JSON extraction funcitons.
|
|
||||||
|
|
||||||
So unless you have a good reason to use MySQL 5.6, you're probably best off
|
|
||||||
using SQLite if you don't have access to a fully supported database version.
|
|
||||||
|
|
||||||
### MySQL 5.6
|
|
||||||
|
|
||||||
** No longer supported. You should really use SQLite if you don't have access to
|
|
||||||
something better. Will most likely never be supported. **
|
|
|
@ -1,100 +0,0 @@
|
||||||
<?php
|
|
||||||
/* Destructr | https://github.com/jobyone/destructr | MIT License */
|
|
||||||
namespace Destructr\LegacyDrivers;
|
|
||||||
|
|
||||||
use Destructr\DSOInterface;
|
|
||||||
use Destructr\Factory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* What this driver supports: Version of SQLite3 in PHP environments that allow
|
|
||||||
* pdo::sqliteCreateFunction
|
|
||||||
*
|
|
||||||
* Overally this driver is quite safe and reliable. Definitely the most safe of
|
|
||||||
* all the legacy drivers. The performance isn't even that bad. It benchmarks
|
|
||||||
* close to the same speed as MySQL 5.7, even. The benchmarks are only operating
|
|
||||||
* on databases of 500 objects though, so ymmv.
|
|
||||||
*/
|
|
||||||
class SQLiteDriver extends AbstractLegacyDriver
|
|
||||||
{
|
|
||||||
public function pdo(\PDO $pdo=null) : ?\PDO
|
|
||||||
{
|
|
||||||
if ($pdo) {
|
|
||||||
$this->pdo = $pdo;
|
|
||||||
/*
|
|
||||||
What we're doing here is adding a custom function to SQLite so that it
|
|
||||||
can extract JSON values. It's not fast, but it does let us use JSON
|
|
||||||
fairly seamlessly.
|
|
||||||
*/
|
|
||||||
$this->pdo->sqliteCreateFunction(
|
|
||||||
'DESTRUCTR_JSON_EXTRACT',
|
|
||||||
'\\Destructr\\LegacyDrivers\\SQLiteDriver::JSON_EXTRACT',
|
|
||||||
2
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return $this->pdo;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function JSON_EXTRACT($json, $path)
|
|
||||||
{
|
|
||||||
$path = substr($path, 2);
|
|
||||||
$path = explode('.', $path);
|
|
||||||
$arr = json_decode($json, true);
|
|
||||||
$out = &$arr;
|
|
||||||
while ($key = array_shift($path)) {
|
|
||||||
if (isset($out[$key])) {
|
|
||||||
$out = &$out[$key];
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return @"$out";
|
|
||||||
}
|
|
||||||
|
|
||||||
public function createTable(string $table, array $virtualColumns) : bool
|
|
||||||
{
|
|
||||||
$sql = $this->sql_ddl([
|
|
||||||
'table'=>$table,
|
|
||||||
]);
|
|
||||||
$out = $this->pdo->exec($sql) !== false;
|
|
||||||
foreach (Factory::CORE_VIRTUAL_COLUMNS as $key => $vcol) {
|
|
||||||
$idxResult = true;
|
|
||||||
if (@$vcol['unique']) {
|
|
||||||
$idxResult = $this->pdo->exec('CREATE UNIQUE INDEX '.$table.'_'.$vcol['name'].'_idx on `'.$table.'`(`'.$vcol['name'].'`)') !== false;
|
|
||||||
} elseif (@$vcol['index']) {
|
|
||||||
$idxResult = $this->pdo->exec('CREATE INDEX '.$table.'_'.$vcol['name'].'_idx on `'.$table.'`(`'.$vcol['name'].'`)') !== false;
|
|
||||||
}
|
|
||||||
if (!$idxResult) {
|
|
||||||
$out = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $out;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function sql_ddl($args=array())
|
|
||||||
{
|
|
||||||
$out = [];
|
|
||||||
$out[] = "CREATE TABLE IF NOT EXISTS `{$args['table']}` (";
|
|
||||||
$lines = [];
|
|
||||||
$lines[] = "`json_data` TEXT DEFAULT NULL";
|
|
||||||
foreach (Factory::CORE_VIRTUAL_COLUMNS as $path => $col) {
|
|
||||||
$line = "`{$col['name']}` {$col['type']}";
|
|
||||||
if (@$col['primary']) {
|
|
||||||
$line .= ' PRIMARY KEY';
|
|
||||||
}
|
|
||||||
$lines[] = $line;
|
|
||||||
}
|
|
||||||
$out[] = implode(','.PHP_EOL, $lines);
|
|
||||||
$out[] = ");";
|
|
||||||
return implode(PHP_EOL, $out);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function expandPath(string $path) : string
|
|
||||||
{
|
|
||||||
return "DESTRUCTR_JSON_EXTRACT(`json_data`,'$.{$path}')";
|
|
||||||
}
|
|
||||||
|
|
||||||
public function json_encode($a, ?array &$b = null, string $prefix = '')
|
|
||||||
{
|
|
||||||
return json_encode($a);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,6 +4,7 @@ declare (strict_types = 1);
|
||||||
namespace Destructr\Drivers;
|
namespace Destructr\Drivers;
|
||||||
|
|
||||||
use Destructr\DSO;
|
use Destructr\DSO;
|
||||||
|
use Destructr\Factory;
|
||||||
use Destructr\Search;
|
use Destructr\Search;
|
||||||
use PHPUnit\DbUnit\TestCaseTrait;
|
use PHPUnit\DbUnit\TestCaseTrait;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
@ -54,15 +55,15 @@ abstract class AbstractDriverTest extends TestCase
|
||||||
$driver = $this->createDriver();
|
$driver = $this->createDriver();
|
||||||
$driver->createTable('testInsert', $this->virtualColumns);
|
$driver->createTable('testInsert', $this->virtualColumns);
|
||||||
//test inserting an object
|
//test inserting an object
|
||||||
$o = new DSO(['dso.id' => 'first-inserted']);
|
$o = new DSO(['dso.id' => 'first-inserted'],new Factory($driver,'no_table'));
|
||||||
$this->assertTrue($driver->insert('testInsert', $o));
|
$this->assertTrue($driver->insert('testInsert', $o));
|
||||||
$this->assertEquals(1, $this->getConnection()->getRowCount('testInsert'));
|
$this->assertEquals(1, $this->getConnection()->getRowCount('testInsert'));
|
||||||
//test inserting a second object
|
//test inserting a second object
|
||||||
$o = new DSO(['dso.id' => 'second-inserted']);
|
$o = new DSO(['dso.id' => 'second-inserted'],new Factory($driver,'no_table'));
|
||||||
$this->assertTrue($driver->insert('testInsert', $o));
|
$this->assertTrue($driver->insert('testInsert', $o));
|
||||||
$this->assertEquals(2, $this->getConnection()->getRowCount('testInsert'));
|
$this->assertEquals(2, $this->getConnection()->getRowCount('testInsert'));
|
||||||
//test inserting a second object with an existing id, it shouldn't work
|
//test inserting a second object with an existing id, it shouldn't work
|
||||||
$o = new DSO(['dso.id' => 'first-inserted']);
|
$o = new DSO(['dso.id' => 'first-inserted'],new Factory($driver,'no_table'));
|
||||||
$this->assertFalse($driver->insert('testInsert', $o));
|
$this->assertFalse($driver->insert('testInsert', $o));
|
||||||
$this->assertEquals(2, $this->getConnection()->getRowCount('testInsert'));
|
$this->assertEquals(2, $this->getConnection()->getRowCount('testInsert'));
|
||||||
}
|
}
|
||||||
|
@ -111,11 +112,11 @@ abstract class AbstractDriverTest extends TestCase
|
||||||
//set up dummy data
|
//set up dummy data
|
||||||
$this->setup_testDelete();
|
$this->setup_testDelete();
|
||||||
//try deleting an item
|
//try deleting an item
|
||||||
$dso = new DSO(['dso.id' => 'item-a-1']);
|
$dso = new DSO(['dso.id' => 'item-a-1'],new Factory($driver,'no_table'));
|
||||||
$driver->delete('testDelete', $dso);
|
$driver->delete('testDelete', $dso);
|
||||||
$this->assertEquals(3, $this->getConnection()->getRowCount('testDelete'));
|
$this->assertEquals(3, $this->getConnection()->getRowCount('testDelete'));
|
||||||
//try deleting an item at the other end of the table
|
//try deleting an item at the other end of the table
|
||||||
$dso = new DSO(['dso.id' => 'item-b-2']);
|
$dso = new DSO(['dso.id' => 'item-b-2'],new Factory($driver,'no_table'));
|
||||||
$driver->delete('testDelete', $dso);
|
$driver->delete('testDelete', $dso);
|
||||||
$this->assertEquals(2, $this->getConnection()->getRowCount('testDelete'));
|
$this->assertEquals(2, $this->getConnection()->getRowCount('testDelete'));
|
||||||
}
|
}
|
||||||
|
@ -127,22 +128,22 @@ abstract class AbstractDriverTest extends TestCase
|
||||||
'dso' => ['id' => 'item-a-1', 'type' => 'type-a'],
|
'dso' => ['id' => 'item-a-1', 'type' => 'type-a'],
|
||||||
'foo' => 'bar',
|
'foo' => 'bar',
|
||||||
'sort' => 'a',
|
'sort' => 'a',
|
||||||
]));
|
],new Factory($driver,'no_table')));
|
||||||
$driver->insert('testDelete', new DSO([
|
$driver->insert('testDelete', new DSO([
|
||||||
'dso' => ['id' => 'item-a-2', 'type' => 'type-a'],
|
'dso' => ['id' => 'item-a-2', 'type' => 'type-a'],
|
||||||
'foo' => 'baz',
|
'foo' => 'baz',
|
||||||
'sort' => 'c',
|
'sort' => 'c',
|
||||||
]));
|
],new Factory($driver,'no_table')));
|
||||||
$driver->insert('testDelete', new DSO([
|
$driver->insert('testDelete', new DSO([
|
||||||
'dso' => ['id' => 'item-b-1', 'type' => 'type-b'],
|
'dso' => ['id' => 'item-b-1', 'type' => 'type-b'],
|
||||||
'foo' => 'buz',
|
'foo' => 'buz',
|
||||||
'sort' => 'b',
|
'sort' => 'b',
|
||||||
]));
|
],new Factory($driver,'no_table')));
|
||||||
$driver->insert('testDelete', new DSO([
|
$driver->insert('testDelete', new DSO([
|
||||||
'dso' => ['id' => 'item-b-2', 'type' => 'type-b', 'deleted' => 100],
|
'dso' => ['id' => 'item-b-2', 'type' => 'type-b', 'deleted' => 100],
|
||||||
'foo' => 'quz',
|
'foo' => 'quz',
|
||||||
'sort' => 'd',
|
'sort' => 'd',
|
||||||
]));
|
],new Factory($driver,'no_table')));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function setup_testSelect()
|
protected function setup_testSelect()
|
||||||
|
@ -152,22 +153,22 @@ abstract class AbstractDriverTest extends TestCase
|
||||||
'dso' => ['id' => 'item-a-1', 'type' => 'type-a'],
|
'dso' => ['id' => 'item-a-1', 'type' => 'type-a'],
|
||||||
'foo' => 'bar',
|
'foo' => 'bar',
|
||||||
'sort' => 'a',
|
'sort' => 'a',
|
||||||
]));
|
],new Factory($driver,'no_table')));
|
||||||
$driver->insert('testSelect', new DSO([
|
$driver->insert('testSelect', new DSO([
|
||||||
'dso' => ['id' => 'item-a-2', 'type' => 'type-a'],
|
'dso' => ['id' => 'item-a-2', 'type' => 'type-a'],
|
||||||
'foo' => 'baz',
|
'foo' => 'baz',
|
||||||
'sort' => 'c',
|
'sort' => 'c',
|
||||||
]));
|
],new Factory($driver,'no_table')));
|
||||||
$driver->insert('testSelect', new DSO([
|
$driver->insert('testSelect', new DSO([
|
||||||
'dso' => ['id' => 'item-b-1', 'type' => 'type-b'],
|
'dso' => ['id' => 'item-b-1', 'type' => 'type-b'],
|
||||||
'foo' => 'buz',
|
'foo' => 'buz',
|
||||||
'sort' => 'b',
|
'sort' => 'b',
|
||||||
]));
|
],new Factory($driver,'no_table')));
|
||||||
$driver->insert('testSelect', new DSO([
|
$driver->insert('testSelect', new DSO([
|
||||||
'dso' => ['id' => 'item-b-2', 'type' => 'type-b', 'deleted' => 100],
|
'dso' => ['id' => 'item-b-2', 'type' => 'type-b', 'deleted' => 100],
|
||||||
'foo' => 'quz',
|
'foo' => 'quz',
|
||||||
'sort' => 'd',
|
'sort' => 'd',
|
||||||
]));
|
],new Factory($driver,'no_table')));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
17
tests/Drivers/SQLite/SQLiteDriverIntegrationTest.php
Normal file
17
tests/Drivers/SQLite/SQLiteDriverIntegrationTest.php
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<?php
|
||||||
|
/* Destructr | https://github.com/jobyone/destructr | MIT License */
|
||||||
|
declare (strict_types = 1);
|
||||||
|
namespace Destructr\Drivers\SQLite;
|
||||||
|
|
||||||
|
use Destructr\Drivers\AbstractDriverIntegrationTest;
|
||||||
|
use Destructr\Drivers\SQLiteDriver;
|
||||||
|
|
||||||
|
class SQLiteDriverIntegrationTest extends AbstractDriverIntegrationTest
|
||||||
|
{
|
||||||
|
const DRIVER_CLASS = SQLiteDriver::class;
|
||||||
|
const DRIVER_DSN = 'sqlite:'.__DIR__.'/integration.test.sqlite';
|
||||||
|
const DRIVER_USERNAME = 'root';
|
||||||
|
const DRIVER_PASSWORD = '';
|
||||||
|
const DRIVER_OPTIONS = null;
|
||||||
|
const TEST_TABLE = 'integrationtest';
|
||||||
|
}
|
|
@ -1,22 +1,16 @@
|
||||||
<?php
|
<?php
|
||||||
/* Destructr | https://github.com/jobyone/destructr | MIT License */
|
/* Destructr | https://github.com/jobyone/destructr | MIT License */
|
||||||
declare(strict_types=1);
|
declare (strict_types = 1);
|
||||||
namespace Destructr\LegacyDrivers\SQLite;
|
namespace Destructr\Drivers\SQLite;
|
||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
use Destructr\Drivers\AbstractDriverTest;
|
use Destructr\Drivers\AbstractDriverTest;
|
||||||
use Destructr\LegacyDrivers\SQLiteDriver;
|
use Destructr\Drivers\SQLiteDriver;
|
||||||
|
|
||||||
class SQLiteDriverTest extends AbstractDriverTest
|
class SQLiteDriverTest extends AbstractDriverTest
|
||||||
{
|
{
|
||||||
const DRIVER_CLASS = SQLiteDriver::class;
|
const DRIVER_CLASS = SQLiteDriver::class;
|
||||||
const DRIVER_DSN = 'sqlite:'.__DIR__.'/driver.test.sqlite';
|
const DRIVER_DSN = 'sqlite:'.__DIR__.'/driver.test.sqlite';
|
||||||
const DRIVER_USERNAME = null;
|
const DRIVER_USERNAME = 'root';
|
||||||
const DRIVER_PASSWORD = null;
|
const DRIVER_PASSWORD = '';
|
||||||
const DRIVER_OPTIONS = null;
|
const DRIVER_OPTIONS = null;
|
||||||
|
|
||||||
public static function setUpBeforeClass()
|
|
||||||
{
|
|
||||||
@unlink(__DIR__.'/driver.test.sqlite');
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -1,23 +0,0 @@
|
||||||
<?php
|
|
||||||
/* Destructr | https://github.com/jobyone/destructr | MIT License */
|
|
||||||
declare(strict_types=1);
|
|
||||||
namespace Destructr\LegacyDrivers\SQLite;
|
|
||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
use Destructr\Drivers\AbstractDriverIntegrationTest;
|
|
||||||
use Destructr\LegacyDrivers\SQLiteDriver;
|
|
||||||
|
|
||||||
class MySQLDriverTest extends AbstractDriverIntegrationTest
|
|
||||||
{
|
|
||||||
const DRIVER_CLASS = SQLiteDriver::class;
|
|
||||||
const DRIVER_DSN = 'sqlite:'.__DIR__.'/integration.test.sqlite';
|
|
||||||
const DRIVER_USERNAME = null;
|
|
||||||
const DRIVER_PASSWORD = null;
|
|
||||||
const DRIVER_OPTIONS = null;
|
|
||||||
const TEST_TABLE = 'sqliteintegrationtest';
|
|
||||||
|
|
||||||
public static function setUpBeforeClass()
|
|
||||||
{
|
|
||||||
@unlink(__DIR__.'/integration.test.sqlite');
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue