SQLite support for virtual columns

This commit is contained in:
Joby 2020-08-27 11:42:11 -06:00
parent 5e9b8b0078
commit 7b125b4d92
17 changed files with 371 additions and 482 deletions

View file

@ -18,9 +18,6 @@
"test": [
"phpunit"
],
"test-local": [
"phpunit --testsuite Local"
],
"test-mysql": [
"phpunit --testsuite MySQL"
],

View file

@ -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

View file

@ -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>

View file

@ -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;

View file

@ -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

View file

@ -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)
{

View file

@ -7,14 +7,14 @@ 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']}` (";
$lines = [];
$lines[] = "`json_data` JSON DEFAULT NULL";
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']) {
$line .= ' PERSISTENT';
} else {
@ -31,7 +31,7 @@ class MariaDBDriver extends MySQLDriver
$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;";
return implode(PHP_EOL, $out);
}

View file

@ -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());
@ -24,25 +24,25 @@ class MySQLDriver extends AbstractDriver
$out = ["SELECT * FROM `{$args['table']}`"];
//where statement
if ($where !== null) {
$out[] = "WHERE ".$where;
$out[] = "WHERE " . $where;
}
//order statement
if ($order !== null) {
$out[] = "ORDER BY ".$order;
$out[] = "ORDER BY " . $order;
}
//limit
if ($limit !== null) {
$out[] = "LIMIT ".$limit;
$out[] = "LIMIT " . $limit;
}
//offset
if ($offset !== null) {
$out[] = "OFFSET ".$offset;
$out[] = "OFFSET " . $offset;
}
//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
$where = $this->expandPaths($args['search']->where());
@ -50,20 +50,20 @@ class MySQLDriver extends AbstractDriver
$out = ["SELECT count(dso_id) FROM `{$args['table']}`"];
//where statement
if ($where !== null) {
$out[] = "WHERE ".$where;
$out[] = "WHERE " . $where;
}
//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[] = "CREATE TABLE IF NOT EXISTS `{$args['table']}` (";
$lines = [];
$lines[] = "`json_data` JSON DEFAULT NULL";
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']) {
$line .= ' STORED';
} else {
@ -80,28 +80,28 @@ class MySQLDriver extends AbstractDriver
$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;";
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}'))";
}
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);";
}
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;';
}
}

View 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);
}
}

View file

@ -33,44 +33,22 @@ class Factory implements DSOFactoryInterface
*/
protected $virtualColumns = [
'dso.id' => [
'name'=>'dso_id',
'type'=>'VARCHAR(16)',
'name' => 'dso_id',
'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)',
'name' => 'dso_type',
'type' => 'VARCHAR(30)',
'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'
]
'name' => 'dso_deleted',
'type' => 'BIGINT',
'index' => 'BTREE',
],
];
public function __construct(Drivers\DSODriverInterface $driver, string $table)
@ -79,7 +57,7 @@ class Factory implements DSOFactoryInterface
$this->table = $table;
}
public function quote(string $str) : string
public function quote(string $str): string
{
return $this->driver->pdo()->quote($str);
}
@ -93,14 +71,14 @@ class Factory implements DSOFactoryInterface
$dso->set('dso.created.date', time());
}
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)
{
$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
* @return string|null
*/
public function class(array $data) : ?string
function class (array $data): ?string
{
return null;
}
public function delete(DSOInterface $dso, bool $permanent = false) : bool
public function delete(DSOInterface $dso, bool $permanent = false): bool
{
if ($permanent) {
return $this->driver->delete($this->table, $dso);
@ -126,13 +104,13 @@ class Factory implements DSOFactoryInterface
return $this->update($dso, true);
}
public function undelete(DSOInterface $dso) : bool
public function undelete(DSOInterface $dso): bool
{
unset($dso['dso.deleted']);
return $this->update($dso, true);
}
public function create(array $data = array()) : DSOInterface
public function create(array $data = array()): DSOInterface
{
if (!($class = $this->class($data))) {
$class = DSO::class;
@ -144,25 +122,25 @@ class Factory implements DSOFactoryInterface
return $dso;
}
public function createTable() : bool
public function createTable(): bool
{
return $this->driver->createTable(
$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) {
$vcols = $this->virtualColumns;
} else {
$vcols = static::CORE_VIRTUAL_COLUMNS;
}
return @$vcols[$path]['name'];
return $this->virtualColumns;
}
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()) {
return true;
@ -176,7 +154,7 @@ class Factory implements DSOFactoryInterface
return $out;
}
public function search() : Search
public function search(): Search
{
return new Search($this);
}
@ -195,7 +173,7 @@ class Factory implements DSOFactoryInterface
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
$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
$search = $this->preprocessSearch($search, $deleted);
@ -220,17 +198,17 @@ class Factory implements DSOFactoryInterface
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->where('${'.$field.'} = :value');
if ($results = $search->execute([':value'=>$value], $deleted)) {
$search->where('${' . $field . '} = :value');
if ($results = $search->execute([':value' => $value], $deleted)) {
return array_shift($results);
}
return null;
}
public function insert(DSOInterface $dso) : bool
public function insert(DSOInterface $dso): bool
{
$this->hook_update($dso);
$dso->hook_update();
@ -255,14 +233,14 @@ class Factory implements DSOFactoryInterface
$added = '${dso.deleted} is null';
}
if ($where) {
$where = '('.$where.') AND '.$added;
$where = '(' . $where . ') AND ' . $added;
} else {
$where = $added;
}
$search->where($where);
}
/* expand virtual column names */
foreach (['where','order'] as $clause) {
foreach (['where', 'order'] as $clause) {
if ($value = $search->$clause()) {
$value = preg_replace_callback(
'/\$\{([^\}\\\]+)\}/',
@ -282,7 +260,7 @@ class Factory implements DSOFactoryInterface
return $search;
}
protected static function generate_id($chars, $length) : string
protected static function generate_id($chars, $length): string
{
$check = new Check();
do {
@ -290,7 +268,7 @@ class Factory implements DSOFactoryInterface
while (strlen($id) < $length) {
$id .= substr(
$chars,
rand(0, strlen($chars)-1),
rand(0, strlen($chars) - 1),
1
);
}

View file

@ -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;
}
}
}
}

View file

@ -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. **

View file

@ -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);
}
}

View file

@ -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')));
}
/**

View 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';
}

View file

@ -1,22 +1,16 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
declare(strict_types=1);
namespace Destructr\LegacyDrivers\SQLite;
declare (strict_types = 1);
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');
}
}

View file

@ -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');
}
}