From 7b125b4d9254540a4ef1e839c97f7d7cf811e1d6 Mon Sep 17 00:00:00 2001 From: Joby Elliott Date: Thu, 27 Aug 2020 11:42:11 -0600 Subject: [PATCH] SQLite support for virtual columns --- composer.json | 3 - examples/sqlite.php | 8 +- phpunit.xml | 7 +- src/DSOFactoryInterface.php | 1 + src/DriverFactory.php | 2 +- src/Drivers/AbstractDriver.php | 9 +- src/Drivers/MariaDBDriver.php | 6 +- src/Drivers/MySQLDriver.php | 36 +-- src/Drivers/SQLiteDriver.php | 260 ++++++++++++++++++ src/Factory.php | 96 +++---- src/LegacyDrivers/AbstractLegacyDriver.php | 197 ------------- src/LegacyDrivers/README.md | 45 --- src/LegacyDrivers/SQLiteDriver.php | 100 ------- tests/Drivers/AbstractDriverTest.php | 27 +- .../SQLite/SQLiteDriverIntegrationTest.php | 17 ++ .../SQLite/SQLiteDriverTest.php | 16 +- .../SQLite/SQLiteDriverIntegrationTest.php | 23 -- 17 files changed, 371 insertions(+), 482 deletions(-) create mode 100644 src/Drivers/SQLiteDriver.php delete mode 100644 src/LegacyDrivers/AbstractLegacyDriver.php delete mode 100644 src/LegacyDrivers/README.md delete mode 100644 src/LegacyDrivers/SQLiteDriver.php create mode 100644 tests/Drivers/SQLite/SQLiteDriverIntegrationTest.php rename tests/{LegacyDrivers => Drivers}/SQLite/SQLiteDriverTest.php (50%) delete mode 100644 tests/LegacyDrivers/SQLite/SQLiteDriverIntegrationTest.php diff --git a/composer.json b/composer.json index 886366b..28b43af 100644 --- a/composer.json +++ b/composer.json @@ -18,9 +18,6 @@ "test": [ "phpunit" ], - "test-local": [ - "phpunit --testsuite Local" - ], "test-mysql": [ "phpunit --testsuite MySQL" ], diff --git a/examples/sqlite.php b/examples/sqlite.php index 031dfde..dba65e7 100644 --- a/examples/sqlite.php +++ b/examples/sqlite.php @@ -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 diff --git a/phpunit.xml b/phpunit.xml index 2264fee..73798ce 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,15 +1,10 @@ - - tests - tests/Drivers - tests/LegacyDrivers - tests/Drivers/MySQL - tests/LegacyDrivers/SQLite + tests/Drivers/SQLite diff --git a/src/DSOFactoryInterface.php b/src/DSOFactoryInterface.php index 2b59c93..9d15345 100644 --- a/src/DSOFactoryInterface.php +++ b/src/DSOFactoryInterface.php @@ -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; diff --git a/src/DriverFactory.php b/src/DriverFactory.php index 8701de0..ed8050a 100644 --- a/src/DriverFactory.php +++ b/src/DriverFactory.php @@ -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 diff --git a/src/Drivers/AbstractDriver.php b/src/Drivers/AbstractDriver.php index ca4ac6f..e9961d5 100644 --- a/src/Drivers/AbstractDriver.php +++ b/src/Drivers/AbstractDriver.php @@ -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) { diff --git a/src/Drivers/MariaDBDriver.php b/src/Drivers/MariaDBDriver.php index ba22784..88cff41 100644 --- a/src/Drivers/MariaDBDriver.php +++ b/src/Drivers/MariaDBDriver.php @@ -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); } diff --git a/src/Drivers/MySQLDriver.php b/src/Drivers/MySQLDriver.php index 48242df..e89f27c 100644 --- a/src/Drivers/MySQLDriver.php +++ b/src/Drivers/MySQLDriver.php @@ -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;'; } } diff --git a/src/Drivers/SQLiteDriver.php b/src/Drivers/SQLiteDriver.php new file mode 100644 index 0000000..5f36fd0 --- /dev/null +++ b/src/Drivers/SQLiteDriver.php @@ -0,0 +1,260 @@ + $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); + } +} diff --git a/src/Factory.php b/src/Factory.php index b31c806..5349a33 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -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 ); } diff --git a/src/LegacyDrivers/AbstractLegacyDriver.php b/src/LegacyDrivers/AbstractLegacyDriver.php deleted file mode 100644 index c813500..0000000 --- a/src/LegacyDrivers/AbstractLegacyDriver.php +++ /dev/null @@ -1,197 +0,0 @@ - $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; - } - } - } -} diff --git a/src/LegacyDrivers/README.md b/src/LegacyDrivers/README.md deleted file mode 100644 index 4a8e361..0000000 --- a/src/LegacyDrivers/README.md +++ /dev/null @@ -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. ** diff --git a/src/LegacyDrivers/SQLiteDriver.php b/src/LegacyDrivers/SQLiteDriver.php deleted file mode 100644 index cb95f3d..0000000 --- a/src/LegacyDrivers/SQLiteDriver.php +++ /dev/null @@ -1,100 +0,0 @@ -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); - } -} diff --git a/tests/Drivers/AbstractDriverTest.php b/tests/Drivers/AbstractDriverTest.php index e75d485..497c638 100644 --- a/tests/Drivers/AbstractDriverTest.php +++ b/tests/Drivers/AbstractDriverTest.php @@ -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'))); } /** diff --git a/tests/Drivers/SQLite/SQLiteDriverIntegrationTest.php b/tests/Drivers/SQLite/SQLiteDriverIntegrationTest.php new file mode 100644 index 0000000..88ad476 --- /dev/null +++ b/tests/Drivers/SQLite/SQLiteDriverIntegrationTest.php @@ -0,0 +1,17 @@ +