From 0d1677a9b4fc7d088cc4a49ca81721189ba1b741 Mon Sep 17 00:00:00 2001 From: Joby Elliott Date: Sat, 29 Aug 2020 15:28:45 -0600 Subject: [PATCH] Schema management (#1) * saves schemas to database, uses those schemas - still need to finish methods for update/add/remove operations on columns, for updating schemas * fix for schema table creation * possible test fixes * possibly working, dropped some non-integration tests that didn't work with schema changes * seems working, but still needs schema updating * mysql's schema updates seem to be working * mariadb driver tests * change to how saving schema for createTable works * very basic tests for schema management * test that schemas are updated in schema table --- .travis.yml | 2 + composer.json | 3 + examples/example_factory.php | 22 +- examples/mariadb.php | 12 +- examples/mysql.php | 52 +++ examples/sqlite.php | 28 +- phpunit.xml | 3 + src/DSO.php | 27 +- src/DSOFactoryInterface.php | 23 -- src/DSOInterface.php | 18 +- src/DriverFactory.php | 4 +- src/Drivers/AbstractDriver.php | 145 +------ src/Drivers/AbstractSQLDriver.php | 353 ++++++++++++++++++ src/Drivers/DSODriverInterface.php | 20 - src/Drivers/MariaDBDriver.php | 57 ++- src/Drivers/MySQLDriver.php | 136 +++---- src/Drivers/SQLiteDriver.php | 184 ++++----- src/Factory.php | 73 +++- src/Search.php | 4 +- ...p => AbstractSQLDriverIntegrationTest.php} | 7 +- .../AbstractSQLDriverSchemaChangeTest.php | 134 +++++++ ...iverTest.php => AbstractSQLDriverTest.php} | 70 +--- tests/Drivers/FactorySchemaA.php | 30 ++ tests/Drivers/FactorySchemaB.php | 30 ++ .../MariaDB/MariaDBDriverIntegrationTest.php | 16 + .../MariaDB/MariaDBDriverSchemaChangeTest.php | 16 + tests/Drivers/MariaDB/MariaDBDriverTest.php | 16 + .../MySQL/MySQLDriverIntegrationTest.php | 7 +- .../MySQL/MySQLDriverSchemaChangeTest.php | 16 + tests/Drivers/MySQL/MySQLDriverTest.php | 6 +- .../SQLite/SQLiteDriverIntegrationTest.php | 10 +- .../SQLite/SQLiteDriverSchemaChangeTest.php | 21 ++ tests/Drivers/SQLite/SQLiteDriverTest.php | 9 +- tests/HarnessDriver.php | 4 +- 34 files changed, 1064 insertions(+), 494 deletions(-) create mode 100644 examples/mysql.php delete mode 100644 src/DSOFactoryInterface.php create mode 100644 src/Drivers/AbstractSQLDriver.php delete mode 100644 src/Drivers/DSODriverInterface.php rename tests/Drivers/{AbstractDriverIntegrationTest.php => AbstractSQLDriverIntegrationTest.php} (96%) create mode 100644 tests/Drivers/AbstractSQLDriverSchemaChangeTest.php rename tests/Drivers/{AbstractDriverTest.php => AbstractSQLDriverTest.php} (67%) create mode 100644 tests/Drivers/FactorySchemaA.php create mode 100644 tests/Drivers/FactorySchemaB.php create mode 100644 tests/Drivers/MariaDB/MariaDBDriverIntegrationTest.php create mode 100644 tests/Drivers/MariaDB/MariaDBDriverSchemaChangeTest.php create mode 100644 tests/Drivers/MariaDB/MariaDBDriverTest.php create mode 100644 tests/Drivers/MySQL/MySQLDriverSchemaChangeTest.php create mode 100644 tests/Drivers/SQLite/SQLiteDriverSchemaChangeTest.php diff --git a/.travis.yml b/.travis.yml index 46f224e..83e5c4f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,8 @@ php: - 7.4 before_install: - mysql -e 'CREATE DATABASE test' + - docker run -d -p 127.0.0.1:3307:3306 --name mysqld -e MYSQL_DATABASE=destructrtest -e MYSQL_USER=destructrtest -e MYSQL_PASSWORD=destructrtest -e MYSQL_ROOT_PASSWORD=verysecret mariadb:10.2 --innodb_log_file_size=256MB --innodb_buffer_pool_size=512MB --max_allowed_packet=16MB + - sleep 15 install: - composer install script: composer test \ No newline at end of file diff --git a/composer.json b/composer.json index 28b43af..0d8a960 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,9 @@ "test-mysql": [ "phpunit --testsuite MySQL" ], + "test-mariadb": [ + "phpunit --testsuite MariaDB" + ], "test-sqlite": [ "phpunit --testsuite SQLite" ] diff --git a/examples/example_factory.php b/examples/example_factory.php index 14869a9..5f1801b 100644 --- a/examples/example_factory.php +++ b/examples/example_factory.php @@ -4,30 +4,26 @@ use Destructr\Factory; class ExampleFactory extends Factory { /** - * Virtual columns are only supported by modern SQL servers. Most of the - * legacy drivers will only use the ones defined in CORE_VIRTUAL_COLUMNS, - * but that should be handled automatically. + * Example factory with a different schema, to index on random_data, but not + * by dso_type. + * + * Also uses a different column name for dso.id */ - protected $virtualColumns = [ + protected $schema = [ 'dso.id' => [ - 'name'=>'dso_id', + 'name'=>'dso_id_other_name', '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', + 'type'=>'INT', 'index'=>'BTREE' ], - 'example.indexed' => [ - 'name'=>'example_indexed', + 'random_data' => [ + 'name'=>'random_data', 'type'=>'VARCHAR(100)', 'index'=>'BTREE' ] diff --git a/examples/mariadb.php b/examples/mariadb.php index 9f3730e..3d88d6c 100644 --- a/examples/mariadb.php +++ b/examples/mariadb.php @@ -15,17 +15,19 @@ $driver = \Destructr\DriverFactory::factoryFromPDO( /* Creates a factory using the table 'example_table', and creates -the necessary table. Note that createTable() can safely be called +the necessary table. Note that prepareEnvironment() can safely be called multiple times. */ -$factory = new \Destructr\Factory($driver, 'example_table'); -$factory->createTable(); +include __DIR__ . '/example_factory.php'; +$factory = new ExampleFactory($driver, 'example_table'); +$factory->prepareEnvironment(); +$factory->updateEnvironment(); /* -The following can be uncommented to insert 1,000 dummy records +The following can be uncommented to insert dummy records into the given table. */ -// for($i = 0; $i < 1000; $i++) { +// for($i = 0; $i < 10; $i++) { // $obj = $factory->create( // [ // 'dso.type'=>'foobar', diff --git a/examples/mysql.php b/examples/mysql.php new file mode 100644 index 0000000..73d09b2 --- /dev/null +++ b/examples/mysql.php @@ -0,0 +1,52 @@ +prepareEnvironment(); +$factory->updateEnvironment(); + +/* +The following can be uncommented to insert dummy records +into the given table. +*/ +// for($i = 0; $i < 100; $i++) { +// $obj = $factory->create( +// [ +// 'dso.type'=>'foobar', +// 'random_data' => md5(rand()) +// ] +// ); +// $obj->insert(); +// } + +/* +Search by random data field +*/ +// $search = $factory->search(); +// $search->where('${random_data} = :q'); +// $result = $search->execute(['q'=>'rw7nivub9bhhh3t4']); + +/* +Search by dso.id, which is much faster because it's indexed +*/ +// $search = $factory->search(); +// $search->where('${dso.id} = :q'); +// $result = $search->execute(['q'=>'rw7nivub9bhhh3t4']); diff --git a/examples/sqlite.php b/examples/sqlite.php index dba65e7..b2a9f65 100644 --- a/examples/sqlite.php +++ b/examples/sqlite.php @@ -1,29 +1,34 @@ createTable(); + */ +include __DIR__ . '/example_factory.php'; +$factory = new Factory($driver, 'example_table'); +$factory->prepareEnvironment(); +$factory->updateEnvironment(); /* -The following can be uncommented to insert 1,000 dummy records +The following can be uncommented to insert dummy records into the given table. -*/ + */ // ini_set('max_execution_time','0'); -// for($i = 0; $i < 1000; $i++) { +// for($i = 0; $i < 10; $i++) { // $obj = $factory->create( // [ // 'dso.type'=>'foobar', @@ -35,18 +40,19 @@ into the given table. /* Search by random data field -*/ + */ $search = $factory->search(); $search->where('${random_data} LIKE :q'); $result = $search->execute(['q'=>'%ab%']); foreach($result as $r) { + var_dump($r->get()); $r['random_data_2'] = md5(rand()); $r->update(); } /* Search by dso.id, which is much faster because it's indexed -*/ + */ // $search = $factory->search(); // $search->where('${dso.id} = :q'); // $result = $search->execute(['q'=>'rw7nivub9bhhh3t4']); diff --git a/phpunit.xml b/phpunit.xml index 73798ce..ec3e652 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -3,6 +3,9 @@ tests/Drivers/MySQL + + tests/Drivers/MariaDB + tests/Drivers/SQLite diff --git a/src/DSO.php b/src/DSO.php index 6000092..16e9503 100644 --- a/src/DSO.php +++ b/src/DSO.php @@ -5,7 +5,7 @@ namespace Destructr; use \Flatrr\FlatArray; /** - * Interface for DeStructure Objects (DSOs). These are the class that is + * Interface for DeStructured Objects (DSOs). These are the class that is * actually used for storing and retrieving partially-structured data from the * database. */ @@ -15,7 +15,7 @@ class DSO extends FlatArray implements DSOInterface protected $changes; protected $removals; - public function __construct(array $data = null, DSOFactoryInterface $factory = null) + public function __construct(array $data = null, Factory $factory = null) { $this->resetChanges(); parent::__construct($data); @@ -32,22 +32,22 @@ class DSO extends FlatArray implements DSOInterface //does nothing } - public function delete(bool $permanent = false) : bool + public function delete(bool $permanent = false): bool { return $this->factory->delete($this, $permanent); } - public function undelete() : bool + public function undelete(): bool { return $this->factory->undelete($this); } - public function insert() : bool + public function insert(): bool { return $this->factory()->insert($this); } - public function update(bool $sneaky = false) : bool + public function update(bool $sneaky = false): bool { return $this->factory()->update($this); } @@ -58,17 +58,17 @@ class DSO extends FlatArray implements DSOInterface $this->removals = new FlatArray(); } - public function changes() : array + public function changes(): array { return $this->changes->get(); } - public function removals() : array + public function removals(): array { return $this->removals->get(); } - public function set(string $name = null, $value, $force=false) + public function set(?string $name, $value, $force = false) { $name = strtolower($name); if ($this->get($name) === $value) { @@ -80,7 +80,7 @@ class DSO extends FlatArray implements DSOInterface foreach ($this->get($name) as $k => $v) { if (!isset($value[$k])) { if ($name) { - $k = $name.'.'.$k; + $k = $name . '.' . $k; } $this->unset($k); } @@ -91,7 +91,7 @@ class DSO extends FlatArray implements DSOInterface //recursively set individual values so we can track them foreach ($value as $k => $v) { if ($name) { - $k = $name.'.'.$k; + $k = $name . '.' . $k; } $this->set($k, $v, $force); } @@ -102,8 +102,7 @@ class DSO extends FlatArray implements DSOInterface } } - public function unset(?string $name) - { + function unset(?string $name) { if (isset($this[$name])) { $this->removals->set($name, $this->get($name)); unset($this->changes[$name]); @@ -111,7 +110,7 @@ class DSO extends FlatArray implements DSOInterface } } - public function factory(DSOFactoryInterface $factory = null) : ?DSOFactoryInterface + public function factory(Factory $factory = null): ?Factory { if ($factory) { $this->factory = $factory; diff --git a/src/DSOFactoryInterface.php b/src/DSOFactoryInterface.php deleted file mode 100644 index 9d15345..0000000 --- a/src/DSOFactoryInterface.php +++ /dev/null @@ -1,23 +0,0 @@ - Drivers\SQLiteDriver::class, ]; - public static function factory(string $dsn, string $username = null, string $password = null, array $options = null, string $type = null): ?Drivers\DSODriverInterface + public static function factory(string $dsn, string $username = null, string $password = null, array $options = null, string $type = null): ?Drivers\AbstractDriver { if (!$type) { $type = @array_shift(explode(':', $dsn, 2)); @@ -23,7 +23,7 @@ class DriverFactory } } - public static function factoryFromPDO(\PDO $pdo, string $type = null): ?Drivers\DSODriverInterface + public static function factoryFromPDO(\PDO $pdo, string $type = null): ?Drivers\AbstractDriver { if (!$type) { $type = $pdo->getAttribute(\PDO::ATTR_DRIVER_NAME); diff --git a/src/Drivers/AbstractDriver.php b/src/Drivers/AbstractDriver.php index e9961d5..09603ef 100644 --- a/src/Drivers/AbstractDriver.php +++ b/src/Drivers/AbstractDriver.php @@ -6,139 +6,16 @@ use Destructr\DSOInterface; use Destructr\Search; use PDO; -abstract class AbstractDriver implements DSODriverInterface +abstract class AbstractDriver { - public $lastPreparationErrorOn; - public $pdo; - - 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) - { - if ($dsn) { - if (!($pdo = new \PDO($dsn, $username, $password, $options))) { - throw new \Exception("Error creating PDO connection"); - } - $this->pdo($pdo); - } - } - - public function pdo(\PDO $pdo = null): ?\PDO - { - if ($pdo) { - $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); - $this->pdo = $pdo; - } - return $this->pdo; - } - - protected function expandPaths($value) - { - if ($value === null) { - return null; - } - $value = preg_replace_callback( - '/\$\{([^\}\\\]+)\}/', - function ($matches) { - return $this->expandPath($matches[1]); - }, - $value - ); - return $value; - } - - public function errorInfo() - { - return $this->pdo->errorInfo(); - } - - public function createTable(string $table, array $virtualColumns): bool - { - $sql = $this->sql_ddl([ - 'table' => $table, - 'virtualColumns' => $virtualColumns, - ]); - return $this->pdo->exec($sql) !== false; - } - - public function update(string $table, DSOInterface $dso): bool - { - if (!$dso->changes() && !$dso->removals()) { - return true; - } - $s = $this->getStatement( - 'setJSON', - ['table' => $table] - ); - return $s->execute([ - ':dso_id' => $dso['dso.id'], - ':data' => json_encode($dso->get()), - ]); - } - - public function delete(string $table, DSOInterface $dso): bool - { - $s = $this->getStatement( - 'delete', - ['table' => $table] - ); - return $s->execute([ - ':dso_id' => $dso['dso.id'], - ]); - } - - public function count(string $table, Search $search, array $params) - { - $s = $this->getStatement( - 'count', - ['table' => $table, 'search' => $search] - ); - if (!$s->execute($params)) { - return null; - } - return intval($s->fetchAll(\PDO::FETCH_COLUMN)[0]); - } - - public function select(string $table, Search $search, array $params) - { - $s = $this->getStatement( - 'select', - ['table' => $table, 'search' => $search] - ); - if (!$s->execute($params)) { - return []; - } - return @$s->fetchAll(\PDO::FETCH_ASSOC); - } - - public function insert(string $table, DSOInterface $dso): bool - { - return $this->getStatement( - 'insert', - ['table' => $table] - )->execute( - [':data' => json_encode($dso->get())] - ); - } - - protected function getStatement(string $type, $args = array()): \PDOStatement - { - $fn = 'sql_' . $type; - if (!method_exists($this, $fn)) { - throw new \Exception("Error getting SQL statement, driver doesn't have a method named $fn"); - } - $sql = $this->$fn($args); - $stmt = $this->pdo->prepare($sql); - if (!$stmt) { - $this->lastPreparationErrorOn = $sql; - throw new \Exception("Error preparing statement: " . implode(': ', $this->pdo->errorInfo()), 1); - } - return $stmt; - } + abstract public function errorInfo(); + abstract public function update(string $table, DSOInterface $dso): bool; + abstract public function delete(string $table, DSOInterface $dso): bool; + abstract public function count(string $table, Search $search, array $params): int; + abstract public function select(string $table, Search $search, array $params); + abstract public function insert(string $table, DSOInterface $dso): bool; + abstract public function prepareEnvironment(string $table, array $schem): bool; + abstract public function beginTransaction(): bool; + abstract public function commit(): bool; + abstract public function rollBack(): bool; } diff --git a/src/Drivers/AbstractSQLDriver.php b/src/Drivers/AbstractSQLDriver.php new file mode 100644 index 0000000..fe9d863 --- /dev/null +++ b/src/Drivers/AbstractSQLDriver.php @@ -0,0 +1,353 @@ +pdo($pdo); + } + } + + public function tableExists(string $table): bool + { + $stmt = $this->pdo()->prepare($this->sql_table_exists($table)); + if ($stmt && $stmt->execute() !== false) { + return true; + } else { + return false; + } + } + + public function createSchemaTable() + { + $this->pdo->exec($this->sql_create_schema_table()); + return $this->tableExists('destructr_schema'); + } + + public function pdo(\PDO $pdo = null): ?\PDO + { + if ($pdo) { + $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); + $this->pdo = $pdo; + } + return $this->pdo; + } + + public function beginTransaction(): bool + { + return $this->pdo->beginTransaction(); + } + + public function commit(): bool + { + return $this->pdo->commit(); + } + + public function rollBack(): bool + { + return $this->pdo->rollBack(); + } + + protected function expandPaths($value) + { + if ($value === null) { + return null; + } + $value = preg_replace_callback( + '/\$\{([^\}\\\]+)\}/', + function ($matches) { + return $this->expandPath($matches[1]); + }, + $value + ); + return $value; + } + + public function errorInfo() + { + return $this->pdo->errorInfo(); + } + + public function prepareEnvironment(string $table, array $schema): bool + { + $this->beginTransaction(); + if ($this->createSchemaTable() && $this->createTable($table, $schema)) { + $this->commit(); + return true; + } else { + $this->rollBack(); + return false; + } + } + + public function updateEnvironment(string $table, array $schema): bool + { + $this->beginTransaction(); + if ($this->updateTable($table, $schema)) { + $this->commit(); + return true; + } else { + $this->rollBack(); + return false; + } + } + + protected function updateTable($table, $schema): bool + { + $current = $this->getSchema($table); + $new = $schema; + if (!$current || $schema == $current) { + return true; + } + //do nothing with totally unchanged columns + foreach ($current as $c_id => $c) { + foreach ($schema as $n_id => $n) { + if ($n == $c && $n_id == $c_id) { + unset($current[$c_id]); + unset($new[$n_id]); + } + } + } + $removed = $current; + $added = $new; + //apply changes + $out = [ + 'removeColumns' => $this->removeColumns($table, $removed), + 'addColumns' => $this->addColumns($table, $added), + 'rebuildSchema' => $this->rebuildSchema($table, $schema), + 'buildIndexes' => $this->buildIndexes($table, $schema), + 'saveSchema' => $this->saveSchema($table, $schema), + ]; + foreach ($out as $k => $v) { + if (!$v) { + user_error("An error occurred during updateTable for $table. The error happened during $k.", E_USER_WARNING); + } + } + return !!array_filter($out); + } + + public function createTable(string $table, array $schema): bool + { + // check if table exists, if it doesn't, save into schema table + $saveSchema = !$this->tableExists($table); + // create table from scratch + $sql = $this->sql_ddl([ + 'table' => $table, + 'schema' => $schema, + ]); + $out = $this->pdo->exec($sql) !== false; + if ($out) { + $this->buildIndexes($table, $schema); + if ($saveSchema) { + $this->saveSchema($table, $schema); + } + } + return $out; + } + + public function getSchema(string $table): ?array + { + if (!isset($this->schemas[$table])) { + $s = $this->getStatement( + 'get_schema', + ['table' => $table] + ); + if (!$s->execute(['table' => $table])) { + $this->schemas[$table] = null; + } else { + if ($row = $s->fetch(\PDO::FETCH_ASSOC)) { + $this->schemas[$table] = @json_decode($row['schema_schema'], true); + } else { + $this->schemas[$table] = null; + } + } + } + return @$this->schemas[$table]; + } + + public function saveSchema(string $table, array $schema): bool + { + $out = $this->pdo->exec( + $this->sql_save_schema($table, $schema) + ) !== false; + unset($this->schemas[$table]); + return $out; + } + + protected function sql_save_schema(string $table, array $schema) + { + $time = time(); + $table = $this->pdo->quote($table); + $schema = $this->pdo->quote(json_encode($schema)); + return <<changes() && !$dso->removals()) { + return true; + } + $s = $this->getStatement( + 'set_json', + ['table' => $table] + ); + return $s->execute([ + ':dso_id' => $dso['dso.id'], + ':data' => json_encode($dso->get()), + ]); + } + + public function delete(string $table, DSOInterface $dso): bool + { + $s = $this->getStatement( + 'delete', + ['table' => $table] + ); + return $s->execute([ + ':dso_id' => $dso['dso.id'], + ]); + } + + public function count(string $table, Search $search, array $params): int + { + $s = $this->getStatement( + 'count', + ['table' => $table, 'search' => $search] + ); + if (!$s->execute($params)) { + return null; + } + return intval($s->fetchAll(\PDO::FETCH_COLUMN)[0]); + } + + public function select(string $table, Search $search, array $params) + { + $s = $this->getStatement( + 'select', + ['table' => $table, 'search' => $search] + ); + if (!$s->execute($params)) { + return []; + } + return @$s->fetchAll(\PDO::FETCH_ASSOC); + } + + public function insert(string $table, DSOInterface $dso): bool + { + return $this->getStatement( + 'insert', + ['table' => $table] + )->execute( + [':data' => json_encode($dso->get())] + ); + } + + protected function getStatement(string $type, $args = array()): \PDOStatement + { + $fn = 'sql_' . $type; + if (!method_exists($this, $fn)) { + throw new \Exception("Error getting SQL statement, driver doesn't have a method named $fn"); + } + $sql = $this->$fn($args); + $stmt = $this->pdo->prepare($sql); + if (!$stmt) { + $this->lastPreparationErrorOn = $sql; + throw new \Exception("Error preparing statement: " . implode(': ', $this->pdo->errorInfo()), 1); + } + return $stmt; + } + + /** + * Within the search we expand strings like ${dso.id} into JSON queries. + * Note that the Search will have already had these strings expanded into + * column names if there are virtual columns configured for them. That + * happens in the Factory before it gets here. + */ + 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) . ';'; + } + + 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_delete(array $args): string + { + return 'DELETE FROM `' . $args['table'] . '` WHERE `dso_id` = :dso_id;'; + } + +} diff --git a/src/Drivers/DSODriverInterface.php b/src/Drivers/DSODriverInterface.php deleted file mode 100644 index 14f2c38..0000000 --- a/src/Drivers/DSODriverInterface.php +++ /dev/null @@ -1,20 +0,0 @@ - $col) { - $line = "`{$col['name']}` {$col['type']} GENERATED ALWAYS AS (" . $this->expandPath($path) . ")"; - if (@$col['primary']) { - $line .= ' PERSISTENT'; - } else { - $line .= ' VIRTUAL'; - } + foreach ($args['schema'] as $path => $col) { + $line = "`{$col['name']}` {$col['type']} GENERATED ALWAYS AS (" . $this->expandPath($path) . ") VIRTUAL"; $lines[] = $line; } - foreach ($args['virtualColumns'] as $path => $col) { - if (@$col['primary']) { - $lines[] = "UNIQUE KEY (`{$col['name']}`)"; - } elseif (@$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); + $out = implode(PHP_EOL, $out); + return $out; + } + + protected function buildIndexes(string $table, array $schema): bool + { + foreach ($schema as $path => $col) { + if (@$col['primary']) { + $this->pdo->exec( + "CREATE UNIQUE INDEX `{$table}_{$col['name']}_idx` ON {$table} (`{$col['name']}`) USING BTREE" + ); + } elseif (@$col['unique'] && $as = @$col['index']) { + $this->pdo->exec( + "CREATE UNIQUE INDEX `{$table}_{$col['name']}_idx` ON {$table} (`{$col['name']}`) USING $as" + ); + } elseif ($as = @$col['index']) { + $this->pdo->exec( + "CREATE INDEX `{$table}_{$col['name']}_idx` ON {$table} (`{$col['name']}`) USING $as" + ); + } + } + return true; + } + + protected function addColumns($table, $schema): bool + { + $out = true; + foreach ($schema as $path => $col) { + $line = "ALTER TABLE `{$table}` ADD COLUMN `${col['name']}` {$col['type']} GENERATED ALWAYS AS (" . $this->expandPath($path) . ")"; + if (@$col['primary']) { + $line .= ' PERSISTENT;'; + } else { + $line .= ' VIRTUAL;'; + } + $out = $out && + $this->pdo->exec($line) !== false; + } + return $out; } } diff --git a/src/Drivers/MySQLDriver.php b/src/Drivers/MySQLDriver.php index e89f27c..202972b 100644 --- a/src/Drivers/MySQLDriver.php +++ b/src/Drivers/MySQLDriver.php @@ -5,64 +5,15 @@ namespace Destructr\Drivers; /** * What this driver supports: MySQL >= 5.7.8 */ -class MySQLDriver extends AbstractDriver +class MySQLDriver extends AbstractSQLDriver { - /** - * Within the search we expand strings like ${dso.id} into JSON queries. - * Note that the Search will have already had these strings expanded into - * column names if there are virtual columns configured for them. That - * happens in the Factory before it gets here. - */ - 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) . ';'; - } - - 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_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) { + foreach ($args['schema'] as $path => $col) { $line = "`{$col['name']}` {$col['type']} GENERATED ALWAYS AS (" . $this->expandPath($path) . ")"; if (@$col['primary']) { $line .= ' STORED'; @@ -71,18 +22,30 @@ class MySQLDriver extends AbstractDriver } $lines[] = $line; } - foreach ($args['virtualColumns'] as $path => $col) { - if (@$col['primary']) { - $lines[] = "PRIMARY KEY (`{$col['name']}`)"; - } elseif (@$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); + $out = implode(PHP_EOL, $out); + return $out; + } + + protected function buildIndexes(string $table, array $schema): bool + { + foreach ($schema as $path => $col) { + if (@$col['primary']) { + $this->pdo->exec( + "CREATE UNIQUE INDEX `{$table}_{$col['name']}_idx` ON {$table} (`{$col['name']}`) USING BTREE" + ); + } elseif (@$col['unique'] && $as = @$col['index']) { + $this->pdo->exec( + "CREATE UNIQUE INDEX `{$table}_{$col['name']}_idx` ON {$table} (`{$col['name']}`) USING $as" + ); + } elseif ($as = @$col['index']) { + $this->pdo->exec( + "CREATE INDEX `{$table}_{$col['name']}_idx` ON {$table} (`{$col['name']}`) USING $as" + ); + } + } + return true; } protected function expandPath(string $path): string @@ -90,7 +53,7 @@ class MySQLDriver extends AbstractDriver return "JSON_UNQUOTE(JSON_EXTRACT(`json_data`,'$.{$path}'))"; } - protected function sql_setJSON(array $args): string + protected function sql_set_json(array $args): string { return 'UPDATE `' . $args['table'] . '` SET `json_data` = :data WHERE `dso_id` = :dso_id;'; } @@ -100,8 +63,53 @@ class MySQLDriver extends AbstractDriver return "INSERT INTO `{$args['table']}` (`json_data`) VALUES (:data);"; } - protected function sql_delete(array $args): string + protected function addColumns($table, $schema): bool { - return 'DELETE FROM `' . $args['table'] . '` WHERE `dso_id` = :dso_id;'; + $out = true; + foreach ($schema as $path => $col) { + $line = "ALTER TABLE `{$table}` ADD COLUMN `${col['name']}` {$col['type']} GENERATED ALWAYS AS (" . $this->expandPath($path) . ")"; + if (@$col['primary']) { + $line .= ' STORED;'; + } else { + $line .= ' VIRTUAL;'; + } + $out = $out && + $this->pdo->exec($line) !== false; + } + return $out; + } + + protected function removeColumns($table, $schema): bool + { + $out = true; + foreach ($schema as $path => $col) { + $out = $out && + $this->pdo->exec("ALTER TABLE `{$table}` DROP COLUMN `${col['name']}`;") !== false; + } + return $out; + } + + protected function rebuildSchema($table, $schema): bool + { + //this does nothing in databases that can generate columns themselves + return true; + } + + protected function sql_create_schema_table(): string + { + return << $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()) { @@ -82,7 +23,7 @@ class SQLiteDriver extends AbstractDriver } $columns = $this->dso_columns($dso); $s = $this->getStatement( - 'setJSON', + 'set_json', [ 'table' => $table, 'columns' => $columns, @@ -104,6 +45,72 @@ class SQLiteDriver extends AbstractDriver return $s->execute($columns); } + protected function updateTable($table, $schema): bool + { + $current = $this->getSchema($table); + if (!$current || $schema == $current) { + return true; + } + //create new table + $table_tmp = "{$table}_tmp_" . md5(rand()); + $sql = $this->sql_ddl([ + 'table' => $table_tmp, + 'schema' => $schema, + ]); + if ($this->pdo->exec($sql) === false) { + return false; + } + //copy data into it + $sql = ["INSERT INTO $table_tmp"]; + $cols = ["json_data"]; + $srcs = ["json_data"]; + foreach ($schema as $path => $col) { + $cols[] = $col['name']; + $srcs[] = $this->expandPath($path); + } + $sql[] = '(' . implode(',', $cols) . ')'; + $sql[] = 'SELECT'; + $sql[] = implode(',', $srcs); + $sql[] = "FROM $table"; + $sql = implode(PHP_EOL, $sql); + if ($this->pdo->exec($sql) === false) { + return false; + } + //remove old table, rename new table to old table + if ($this->pdo->exec("DROP TABLE $table") === false) { + return false; + } + if ($this->pdo->exec("ALTER TABLE $table_tmp RENAME TO $table") === false) { + return false; + } + //set up indexes + if (!$this->buildIndexes($table, $schema)) { + return false; + } + //save schema + $this->saveSchema($table, $schema); + //return result + return true; + } + + protected function addColumns($table, $schema): bool + { + //does nothing + return true; + } + + protected function removeColumns($table, $schema): bool + { + //does nothing + return true; + } + + protected function rebuildSchema($table, $schema): bool + { + //does nothing + return true; + } + protected function sql_insert(array $args): string { $out = []; @@ -120,18 +127,18 @@ class SQLiteDriver extends AbstractDriver return $out; } - protected function sql_setJSON(array $args): string + protected function sql_set_json(array $args): string { $names = array_map( function ($e) { - return '`'.preg_replace('/^:/', '', $e).'` = '.$e; + return '`' . preg_replace('/^:/', '', $e) . '` = ' . $e; }, array_keys($args['columns']) ); $out = []; $out[] = 'UPDATE `' . $args['table'] . '`'; $out[] = 'SET'; - $out[] = implode(','.PHP_EOL,$names); + $out[] = implode(',' . PHP_EOL, $names); $out[] = 'WHERE `dso_id` = :dso_id'; $out = implode(PHP_EOL, $out) . ';'; return $out; @@ -148,11 +155,6 @@ class SQLiteDriver extends AbstractDriver ]); } - 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. @@ -162,8 +164,8 @@ class SQLiteDriver extends AbstractDriver */ protected function dso_columns(DSOInterface $dso) { - $columns = [':json_data' => $this->json_encode($dso->get())]; - foreach ($dso->factory()->virtualColumns() as $vk => $vv) { + $columns = [':json_data' => json_encode($dso->get())]; + foreach ($this->getSchema($dso->factory()->table()) ?? [] as $vk => $vv) { $columns[':' . $vv['name']] = $dso->get($vk); } return $columns; @@ -206,27 +208,21 @@ class SQLiteDriver extends AbstractDriver return @"$out"; } - public function createTable(string $table, array $virtualColumns): bool + protected function buildIndexes(string $table, array $schema): bool { - $sql = $this->sql_ddl([ - 'table' => $table, - 'virtualColumns' => $virtualColumns, - ]); - $out = $this->pdo->exec($sql) !== false; - foreach ($virtualColumns as $key => $vcol) { - $idxResult = true; + $result = true; + foreach ($schema as $key => $vcol) { 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; + $result = $result && + $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; + $idxResult = $result && + $this->pdo->exec('CREATE INDEX ' . $table . '_' . $vcol['name'] . '_idx on `' . $table . '`(`' . $vcol['name'] . '`)') !== false; } } - return $out; + return $result; } protected function sql_ddl(array $args = []): string @@ -235,7 +231,7 @@ class SQLiteDriver extends AbstractDriver $out[] = "CREATE TABLE IF NOT EXISTS `{$args['table']}` ("; $lines = []; $lines[] = "`json_data` TEXT DEFAULT NULL"; - foreach ($args['virtualColumns'] as $path => $col) { + foreach ($args['schema'] as $path => $col) { $line = "`{$col['name']}` {$col['type']}"; if (@$col['primary']) { $line .= ' PRIMARY KEY'; @@ -253,8 +249,20 @@ class SQLiteDriver extends AbstractDriver return "DESTRUCTR_JSON_EXTRACT(`json_data`,'$.{$path}')"; } - public function json_encode($a, ?array &$b = null, string $prefix = '') + protected function sql_create_schema_table(): string { - return json_encode($a); + return << [ - 'name' => 'dso_id', - 'type' => 'VARCHAR(16)', - 'index' => 'BTREE', - 'unique' => true, - 'primary' => true, + 'name' => 'dso_id', //column name to be used + 'type' => 'VARCHAR(16)', //column type + 'index' => 'BTREE', //whether/how to index + 'unique' => true, //whether column should be unique + 'primary' => true, //whether column should be the primary key ], 'dso.type' => [ 'name' => 'dso_type', @@ -51,12 +57,33 @@ class Factory implements DSOFactoryInterface ], ]; - public function __construct(Drivers\DSODriverInterface $driver, string $table) + public function __construct(Drivers\AbstractDriver $driver, string $table) { $this->driver = $driver; $this->table = $table; } + public function table(): string + { + return $this->table; + } + + public function driver(): AbstractDriver + { + return $this->driver; + } + + public function tableExists(): bool + { + return $this->driver->tableExists($this->table); + } + + public function createSchemaTable(): bool + { + $this->driver->createSchemaTable('destructr_schema'); + return $this->driver->tableExists('destructr_schema'); + } + public function quote(string $str): string { return $this->driver->pdo()->quote($str); @@ -122,22 +149,30 @@ class Factory implements DSOFactoryInterface return $dso; } - public function createTable(): bool + public function prepareEnvironment(): bool { - return $this->driver->createTable( + return $this->driver->prepareEnvironment( $this->table, - $this->virtualColumns + $this->schema ); } - public function virtualColumns(): array + public function updateEnvironment(): bool { - return $this->virtualColumns; + return $this->driver->updateEnvironment( + $this->table, + $this->schema + ); + } + + public function schema(): array + { + return $this->driver->getSchema($this->table) ?? $this->schema; } protected function virtualColumnName($path): ?string { - return @$this->virtualColumns[$path]['name']; + return @$this->schema()[$path]['name']; } public function update(DSOInterface $dso, bool $sneaky = false): bool diff --git a/src/Search.php b/src/Search.php index d48dce1..023631f 100644 --- a/src/Search.php +++ b/src/Search.php @@ -2,8 +2,6 @@ /* Destructr | https://github.com/jobyone/destructr | MIT License */ namespace Destructr; -use Destructr\DSOFactoryInterface; - class Search implements \Serializable { protected $factory; @@ -12,7 +10,7 @@ class Search implements \Serializable protected $limit; protected $offset; - public function __construct(DSOFactoryInterface $factory = null) + public function __construct(Factory $factory = null) { $this->factory = $factory; } diff --git a/tests/Drivers/AbstractDriverIntegrationTest.php b/tests/Drivers/AbstractSQLDriverIntegrationTest.php similarity index 96% rename from tests/Drivers/AbstractDriverIntegrationTest.php rename to tests/Drivers/AbstractSQLDriverIntegrationTest.php index cdfebee..634d35e 100644 --- a/tests/Drivers/AbstractDriverIntegrationTest.php +++ b/tests/Drivers/AbstractSQLDriverIntegrationTest.php @@ -7,9 +7,10 @@ use Destructr\Factory; use PHPUnit\DbUnit\TestCaseTrait; use PHPUnit\Framework\TestCase; -abstract class AbstractDriverIntegrationTest extends TestCase +abstract class AbstractSQLDriverIntegrationTest extends TestCase { use TestCaseTrait; + const TEST_TABLE = 'integrationtest'; public static function setUpBeforeClass() { @@ -17,10 +18,10 @@ abstract class AbstractDriverIntegrationTest extends TestCase $pdo->exec('DROP TABLE ' . static::TEST_TABLE); } - public function testCreateTable() + public function testPrepareEnvironment() { $factory = $this->createFactory(); - $factory->createTable(); + $factory->prepareEnvironment(); //table should exist and have zero rows $this->assertEquals(0, $this->getConnection()->getRowCount(static::TEST_TABLE)); } diff --git a/tests/Drivers/AbstractSQLDriverSchemaChangeTest.php b/tests/Drivers/AbstractSQLDriverSchemaChangeTest.php new file mode 100644 index 0000000..4f4fec3 --- /dev/null +++ b/tests/Drivers/AbstractSQLDriverSchemaChangeTest.php @@ -0,0 +1,134 @@ +createFactoryA(); + $factory->prepareEnvironment(); + $factory->updateEnvironment(); + // verify schema in database + $this->assertEquals( + $factory->schema, + $factory->driver()->getSchema('schematest') + ); + // add some content + $new = $factory->create([ + 'dso.id' => 'dso1', + 'test.a' => 'value a1', + 'test.b' => 'value b1', + 'test.c' => 'value c1', + ]); + $new->insert(); + $new = $factory->create([ + 'dso.id' => 'dso2', + 'test.a' => 'value a2', + 'test.b' => 'value b2', + 'test.c' => 'value c2', + ]); + $new->insert(); + $new = $factory->create([ + 'dso.id' => 'dso3', + 'test.a' => 'value a3', + 'test.b' => 'value b3', + 'test.c' => 'value c3', + ]); + $new->insert(); + // verify data in table matches + $pdo = $this->createPDO(); + $this->assertEquals(3, $this->getConnection()->getRowCount('schematest')); + for ($i = 1; $i <= 3; $i++) { + $row = $pdo->query('select dso_id, test_a, test_b from schematest where dso_id = "dso' . $i . '"')->fetch(PDO::FETCH_ASSOC); + $this->assertEquals(['dso_id' => "dso$i", 'test_a' => "value a$i", 'test_b' => "value b$i"], $row); + } + // change to schema B + sleep(1); //a table can't have its schema updated faster than once per second + $factory = $this->createFactoryB(); + $factory->prepareEnvironment(); + $factory->updateEnvironment(); + // verify schema in database + $this->assertEquals( + $factory->schema, + $factory->driver()->getSchema('schematest') + ); + // verify data in table matches + $pdo = $this->createPDO(); + $this->assertEquals(3, $this->getConnection()->getRowCount('schematest')); + for ($i = 1; $i <= 3; $i++) { + $row = $pdo->query('select dso_id, test_a_2, test_c from schematest where dso_id = "dso' . $i . '"')->fetch(PDO::FETCH_ASSOC); + $this->assertEquals(['dso_id' => "dso$i", 'test_a_2' => "value a$i", 'test_c' => "value c$i"], $row); + } + } + + protected static function createFactoryA() + { + $driver = static::createDriver(); + $factory = new FactorySchemaA( + $driver, + static::TEST_TABLE + ); + return $factory; + } + + protected static function createFactoryB() + { + $driver = static::createDriver(); + $factory = new FactorySchemaB( + $driver, + static::TEST_TABLE + ); + return $factory; + } + + protected static function createDriver() + { + $class = static::DRIVER_CLASS; + return new $class( + static::DRIVER_DSN, + static::DRIVER_USERNAME, + static::DRIVER_PASSWORD, + static::DRIVER_OPTIONS + ); + } + + protected static function createPDO() + { + return new \PDO( + static::DRIVER_DSN, + static::DRIVER_USERNAME, + static::DRIVER_PASSWORD, + static::DRIVER_OPTIONS + ); + } + + public function getConnection() + { + return $this->createDefaultDBConnection($this->createPDO(), 'phpunit'); + } + + public function getDataSet() + { + return new \PHPUnit\DbUnit\DataSet\DefaultDataSet(); + } + + public static function setUpBeforeClass() + { + $pdo = static::createPDO(); + $pdo->exec('DROP TABLE schematest'); + $pdo->exec('DROP TABLE destructr_schema'); + } +} diff --git a/tests/Drivers/AbstractDriverTest.php b/tests/Drivers/AbstractSQLDriverTest.php similarity index 67% rename from tests/Drivers/AbstractDriverTest.php rename to tests/Drivers/AbstractSQLDriverTest.php index 497c638..5bb0744 100644 --- a/tests/Drivers/AbstractDriverTest.php +++ b/tests/Drivers/AbstractSQLDriverTest.php @@ -13,18 +13,18 @@ use PHPUnit\Framework\TestCase; * This class tests a factory in isolation. In the name of simplicity it's a bit * simplistic, because it doesn't get the help of the Factory. * - * There is also a class called AbstractDriverIntegrationTest that tests drivers + * There is also a class called AbstractSQLDriverIntegrationTest that tests drivers * through a Factory. The results of that are harder to interpret, but more * properly and thoroughly test the Drivers in a real environment. */ -abstract class AbstractDriverTest extends TestCase +abstract class AbstractSQLDriverTest extends TestCase { use TestCaseTrait; /* In actual practice, these would come from a Factory */ - protected $virtualColumns = [ + protected $schema = [ 'dso.id' => [ 'name' => 'dso_id', 'type' => 'VARCHAR(16)', @@ -43,17 +43,22 @@ abstract class AbstractDriverTest extends TestCase ], ]; - public function testCreateTable() + public function testPrepareEnvironment() { $driver = $this->createDriver(); - $driver->createTable('testCreateTable', $this->virtualColumns); - $this->assertEquals(0, $this->getConnection()->getRowCount('testCreateTable')); + $this->assertFalse($driver->tableExists('testPrepareEnvironment')); + $this->assertFalse($driver->tableExists('destructr_schema')); + $driver->prepareEnvironment('testPrepareEnvironment', $this->schema); + $this->assertTrue($driver->tableExists('destructr_schema')); + $this->assertTrue($driver->tableExists('testPrepareEnvironment')); + $this->assertEquals(1, $this->getConnection()->getRowCount('destructr_schema')); + $this->assertEquals(0, $this->getConnection()->getRowCount('testPrepareEnvironment')); } public function testInsert() { $driver = $this->createDriver(); - $driver->createTable('testInsert', $this->virtualColumns); + $driver->prepareEnvironment('testInsert', $this->schema); //test inserting an object $o = new DSO(['dso.id' => 'first-inserted'],new Factory($driver,'no_table')); $this->assertTrue($driver->insert('testInsert', $o)); @@ -62,16 +67,12 @@ abstract class AbstractDriverTest extends TestCase $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'],new Factory($driver,'no_table')); - $this->assertFalse($driver->insert('testInsert', $o)); - $this->assertEquals(2, $this->getConnection()->getRowCount('testInsert')); } public function testSelect() { $driver = $this->createDriver(); - $driver->createTable('testSelect', $this->virtualColumns); + $driver->prepareEnvironment('testSelect', $this->schema); //set up dummy data $this->setup_testSelect(); //empty search @@ -105,47 +106,6 @@ abstract class AbstractDriverTest extends TestCase $this->assertSame(0, count($results)); } - public function testDelete() - { - $driver = $this->createDriver(); - $driver->createTable('testDelete', $this->virtualColumns); - //set up dummy data - $this->setup_testDelete(); - //try deleting an item - $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'],new Factory($driver,'no_table')); - $driver->delete('testDelete', $dso); - $this->assertEquals(2, $this->getConnection()->getRowCount('testDelete')); - } - - protected function setup_testDelete() - { - $driver = $this->createDriver(); - $driver->insert('testDelete', new DSO([ - '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() { $driver = $this->createDriver(); @@ -189,10 +149,10 @@ abstract class AbstractDriverTest extends TestCase public static function setUpBeforeClass() { $pdo = static::createPDO(); - $pdo->exec('DROP TABLE testCreateTable'); + $pdo->exec('DROP TABLE testPrepareEnvironment'); $pdo->exec('DROP TABLE testInsert'); $pdo->exec('DROP TABLE testSelect'); - $pdo->exec('DROP TABLE testDelete'); + $pdo->exec('DROP TABLE destructr_schema'); } protected static function createPDO() diff --git a/tests/Drivers/FactorySchemaA.php b/tests/Drivers/FactorySchemaA.php new file mode 100644 index 0000000..69ba807 --- /dev/null +++ b/tests/Drivers/FactorySchemaA.php @@ -0,0 +1,30 @@ + [ + 'name' => 'dso_id', //column name to be used + 'type' => 'VARCHAR(16)', //column type + 'index' => 'BTREE', //whether/how to index + 'unique' => true, //whether column should be unique + 'primary' => true, //whether column should be the primary key + ], + 'test.a' => [ + 'name' => 'test_a', + 'type' => 'VARCHAR(100)', + 'index' => 'BTREE', + ] + , + 'test.b' => [ + 'name' => 'test_b', + 'type' => 'VARCHAR(100)', + 'index' => 'BTREE', + ], + ]; +} diff --git a/tests/Drivers/FactorySchemaB.php b/tests/Drivers/FactorySchemaB.php new file mode 100644 index 0000000..aa4dd95 --- /dev/null +++ b/tests/Drivers/FactorySchemaB.php @@ -0,0 +1,30 @@ + [ + 'name' => 'dso_id', //column name to be used + 'type' => 'VARCHAR(16)', //column type + 'index' => 'BTREE', //whether/how to index + 'unique' => true, //whether column should be unique + 'primary' => true, //whether column should be the primary key + ], + 'test.a' => [ + 'name' => 'test_a_2', + 'type' => 'VARCHAR(100)', + 'index' => 'BTREE', + ] + , + 'test.c' => [ + 'name' => 'test_c', + 'type' => 'VARCHAR(100)', + 'index' => 'BTREE', + ], + ]; +} diff --git a/tests/Drivers/MariaDB/MariaDBDriverIntegrationTest.php b/tests/Drivers/MariaDB/MariaDBDriverIntegrationTest.php new file mode 100644 index 0000000..b3882aa --- /dev/null +++ b/tests/Drivers/MariaDB/MariaDBDriverIntegrationTest.php @@ -0,0 +1,16 @@ +