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": [
|
||||
"phpunit"
|
||||
],
|
||||
"test-local": [
|
||||
"phpunit --testsuite Local"
|
||||
],
|
||||
"test-mysql": [
|
||||
"phpunit --testsuite MySQL"
|
||||
],
|
||||
|
|
|
@ -37,8 +37,12 @@ into the given table.
|
|||
Search by random data field
|
||||
*/
|
||||
$search = $factory->search();
|
||||
$search->where('${random_data} = :q');
|
||||
$result = $search->execute(['q'=>'rw7nivub9bhhh3t4']);
|
||||
$search->where('${random_data} LIKE :q');
|
||||
$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
|
||||
|
|
|
@ -1,15 +1,10 @@
|
|||
<phpunit bootstrap="vendor/autoload.php">
|
||||
<testsuites>
|
||||
<testsuite name="Local">
|
||||
<directory>tests</directory>
|
||||
<exclude>tests/Drivers</exclude>
|
||||
<exclude>tests/LegacyDrivers</exclude>
|
||||
</testsuite>
|
||||
<testsuite name="MySQL">
|
||||
<directory>tests/Drivers/MySQL</directory>
|
||||
</testsuite>
|
||||
<testsuite name="SQLite">
|
||||
<directory>tests/LegacyDrivers/SQLite</directory>
|
||||
<directory>tests/Drivers/SQLite</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
</phpunit>
|
||||
|
|
|
@ -7,6 +7,7 @@ interface DSOFactoryInterface
|
|||
public function __construct(Drivers\DSODriverInterface $driver, string $table);
|
||||
|
||||
public function class(array $data) : ?string;
|
||||
public function virtualColumns(): array;
|
||||
|
||||
public function createTable() : bool;
|
||||
public function create(array $data = array()) : DSOInterface;
|
||||
|
|
|
@ -7,7 +7,7 @@ class DriverFactory
|
|||
public static $map = [
|
||||
'mariadb' => Drivers\MariaDBDriver::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
|
||||
|
|
|
@ -10,7 +10,14 @@ abstract class AbstractDriver implements DSODriverInterface
|
|||
{
|
||||
public $lastPreparationErrorOn;
|
||||
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)
|
||||
{
|
||||
|
|
|
@ -7,7 +7,7 @@ namespace Destructr\Drivers;
|
|||
*/
|
||||
class MariaDBDriver extends MySQLDriver
|
||||
{
|
||||
protected function sql_ddl($args=array())
|
||||
protected function sql_ddl(array $args = []): string
|
||||
{
|
||||
$out = [];
|
||||
$out[] = "CREATE TABLE IF NOT EXISTS `{$args['table']}` (";
|
||||
|
|
|
@ -13,7 +13,7 @@ class MySQLDriver extends AbstractDriver
|
|||
* column names if there are virtual columns configured for them. That
|
||||
* 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
|
||||
$where = $this->expandPaths($args['search']->where());
|
||||
|
@ -42,7 +42,7 @@ class MySQLDriver extends AbstractDriver
|
|||
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
|
||||
$where = $this->expandPaths($args['search']->where());
|
||||
|
@ -56,7 +56,7 @@ class MySQLDriver extends AbstractDriver
|
|||
return implode(PHP_EOL, $out) . ';';
|
||||
}
|
||||
|
||||
protected function sql_ddl($args=array())
|
||||
protected function sql_ddl(array $args = []): string
|
||||
{
|
||||
$out = [];
|
||||
$out[] = "CREATE TABLE IF NOT EXISTS `{$args['table']}` (";
|
||||
|
@ -90,17 +90,17 @@ class MySQLDriver extends AbstractDriver
|
|||
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;';
|
||||
}
|
||||
|
||||
protected function sql_insert($args)
|
||||
protected function sql_insert(array $args): string
|
||||
{
|
||||
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;';
|
||||
}
|
||||
|
|
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);
|
||||
}
|
||||
}
|
|
@ -37,40 +37,18 @@ class Factory implements DSOFactoryInterface
|
|||
'type' => 'VARCHAR(16)',
|
||||
'index' => 'BTREE',
|
||||
'unique' => true,
|
||||
'primary' => true
|
||||
'primary' => true,
|
||||
],
|
||||
'dso.type' => [
|
||||
'name' => 'dso_type',
|
||||
'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',
|
||||
'unique' => true,
|
||||
'primary' => true
|
||||
],
|
||||
'dso.type' => [
|
||||
'name'=>'dso_type',
|
||||
'type'=>'VARCHAR(30)',
|
||||
'index'=>'BTREE'
|
||||
],
|
||||
'dso.deleted' => [
|
||||
'name' => 'dso_deleted',
|
||||
'type' => 'BIGINT',
|
||||
'index'=>'BTREE'
|
||||
]
|
||||
'index' => 'BTREE',
|
||||
],
|
||||
];
|
||||
|
||||
public function __construct(Drivers\DSODriverInterface $driver, string $table)
|
||||
|
@ -112,7 +90,7 @@ class Factory implements DSOFactoryInterface
|
|||
* @param array $data
|
||||
* @return string|null
|
||||
*/
|
||||
public function class(array $data) : ?string
|
||||
function class (array $data): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
@ -148,18 +126,18 @@ class Factory implements DSOFactoryInterface
|
|||
{
|
||||
return $this->driver->createTable(
|
||||
$this->table,
|
||||
($this->driver::EXTENSIBLE_VIRTUAL_COLUMNS?$this->virtualColumns:$this::CORE_VIRTUAL_COLUMNS)
|
||||
$this->virtualColumns
|
||||
);
|
||||
}
|
||||
|
||||
public function virtualColumns(): array
|
||||
{
|
||||
return $this->virtualColumns;
|
||||
}
|
||||
|
||||
protected function virtualColumnName($path): ?string
|
||||
{
|
||||
if ($this->driver::EXTENSIBLE_VIRTUAL_COLUMNS) {
|
||||
$vcols = $this->virtualColumns;
|
||||
} else {
|
||||
$vcols = static::CORE_VIRTUAL_COLUMNS;
|
||||
}
|
||||
return @$vcols[$path]['name'];
|
||||
return @$this->virtualColumns[$path]['name'];
|
||||
}
|
||||
|
||||
public function update(DSOInterface $dso, bool $sneaky = false): bool
|
||||
|
|
|
@ -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;
|
||||
|
||||
use Destructr\DSO;
|
||||
use Destructr\Factory;
|
||||
use Destructr\Search;
|
||||
use PHPUnit\DbUnit\TestCaseTrait;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
@ -54,15 +55,15 @@ abstract class AbstractDriverTest extends TestCase
|
|||
$driver = $this->createDriver();
|
||||
$driver->createTable('testInsert', $this->virtualColumns);
|
||||
//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->assertEquals(1, $this->getConnection()->getRowCount('testInsert'));
|
||||
//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->assertEquals(2, $this->getConnection()->getRowCount('testInsert'));
|
||||
//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->assertEquals(2, $this->getConnection()->getRowCount('testInsert'));
|
||||
}
|
||||
|
@ -111,11 +112,11 @@ abstract class AbstractDriverTest extends TestCase
|
|||
//set up dummy data
|
||||
$this->setup_testDelete();
|
||||
//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);
|
||||
$this->assertEquals(3, $this->getConnection()->getRowCount('testDelete'));
|
||||
//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);
|
||||
$this->assertEquals(2, $this->getConnection()->getRowCount('testDelete'));
|
||||
}
|
||||
|
@ -127,22 +128,22 @@ abstract class AbstractDriverTest extends TestCase
|
|||
'dso' => ['id' => 'item-a-1', 'type' => 'type-a'],
|
||||
'foo' => 'bar',
|
||||
'sort' => 'a',
|
||||
]));
|
||||
],new Factory($driver,'no_table')));
|
||||
$driver->insert('testDelete', new DSO([
|
||||
'dso' => ['id' => 'item-a-2', 'type' => 'type-a'],
|
||||
'foo' => 'baz',
|
||||
'sort' => 'c',
|
||||
]));
|
||||
],new Factory($driver,'no_table')));
|
||||
$driver->insert('testDelete', new DSO([
|
||||
'dso' => ['id' => 'item-b-1', 'type' => 'type-b'],
|
||||
'foo' => 'buz',
|
||||
'sort' => 'b',
|
||||
]));
|
||||
],new Factory($driver,'no_table')));
|
||||
$driver->insert('testDelete', new DSO([
|
||||
'dso' => ['id' => 'item-b-2', 'type' => 'type-b', 'deleted' => 100],
|
||||
'foo' => 'quz',
|
||||
'sort' => 'd',
|
||||
]));
|
||||
],new Factory($driver,'no_table')));
|
||||
}
|
||||
|
||||
protected function setup_testSelect()
|
||||
|
@ -152,22 +153,22 @@ abstract class AbstractDriverTest extends TestCase
|
|||
'dso' => ['id' => 'item-a-1', 'type' => 'type-a'],
|
||||
'foo' => 'bar',
|
||||
'sort' => 'a',
|
||||
]));
|
||||
],new Factory($driver,'no_table')));
|
||||
$driver->insert('testSelect', new DSO([
|
||||
'dso' => ['id' => 'item-a-2', 'type' => 'type-a'],
|
||||
'foo' => 'baz',
|
||||
'sort' => 'c',
|
||||
]));
|
||||
],new Factory($driver,'no_table')));
|
||||
$driver->insert('testSelect', new DSO([
|
||||
'dso' => ['id' => 'item-b-1', 'type' => 'type-b'],
|
||||
'foo' => 'buz',
|
||||
'sort' => 'b',
|
||||
]));
|
||||
],new Factory($driver,'no_table')));
|
||||
$driver->insert('testSelect', new DSO([
|
||||
'dso' => ['id' => 'item-b-2', 'type' => 'type-b', 'deleted' => 100],
|
||||
'foo' => 'quz',
|
||||
'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
|
||||
/* Destructr | https://github.com/jobyone/destructr | MIT License */
|
||||
declare (strict_types = 1);
|
||||
namespace Destructr\LegacyDrivers\SQLite;
|
||||
namespace Destructr\Drivers\SQLite;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Destructr\Drivers\AbstractDriverTest;
|
||||
use Destructr\LegacyDrivers\SQLiteDriver;
|
||||
use Destructr\Drivers\SQLiteDriver;
|
||||
|
||||
class SQLiteDriverTest extends AbstractDriverTest
|
||||
{
|
||||
const DRIVER_CLASS = SQLiteDriver::class;
|
||||
const DRIVER_DSN = 'sqlite:'.__DIR__.'/driver.test.sqlite';
|
||||
const DRIVER_USERNAME = null;
|
||||
const DRIVER_PASSWORD = null;
|
||||
const DRIVER_USERNAME = 'root';
|
||||
const DRIVER_PASSWORD = '';
|
||||
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