Compare commits

...

31 commits

Author SHA1 Message Date
Joby Elliott
40e1d5df26 enable disabling transactions 2022-06-08 11:11:28 -06:00
4018095088
Better transaction handling 2022-06-08 10:28:45 -06:00
11e6e43072
handle PDO exceptions in createSchemaTable 2022-06-06 13:05:38 -06:00
da7660fa20
V1.6 (#3)
* changes to id generation
* handling cases where PDO throws exceptions
2022-06-04 14:13:20 -06:00
Joby Elliott
f9a95898d4
Test overhaul (#2)
* first attempt at PHPUnit tests in github actions

* composer ignore platform deps

* another try

* plumbing

* pdo auth

* more plumbing

* doing env variables right

* will this work?

* calling phpunit directly

* don't specify test

* export

* bug fix

* trying something

* trying another thing

* trying again

* trying it right

* might work this time

* testing can't work on 7.0
2022-05-27 10:44:41 -06:00
Joby Elliott
2f94e2f7eb
Update Factory.php 2020-09-22 09:28:54 -06:00
d5dcb941a4 factory method for checking if environment needs updating 2020-08-29 20:59:27 -06:00
Joby Elliott
16fbacf072
legacy handling
save current schema if no schema is saved for an existing table
2020-08-29 16:23:51 -06:00
110c11dbb2 updated and simplified examples 2020-08-29 15:54:37 -06:00
Joby Elliott
0d1677a9b4
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
2020-08-29 15:28:45 -06:00
bc3fe54e1c updated readme, new Search helper methods 2020-08-28 09:36:21 -06:00
fc506d38a2 update to readme 2020-08-27 11:46:13 -06:00
7b125b4d92 SQLite support for virtual columns 2020-08-27 11:42:11 -06:00
5e9b8b0078 cleanup, examples 2020-08-26 11:53:27 -06:00
a452a5f90b removing defunct tests 2020-08-26 10:02:24 -06:00
d99c09e575 exception error mode actually won't work 2020-08-26 09:58:08 -06:00
5526097af4 Merge branch 'main' of github.com:jobyone/destructr into main 2020-08-26 09:53:06 -06:00
4bf591804c style fixes, create table fixes, MariaDB fixes 2020-08-26 09:52:25 -06:00
e088cc47aa fixing test 2020-07-23 12:10:17 -06:00
922eac342d removing incorrect references 2020-07-23 12:05:52 -06:00
62102fbf9f tests are working again 2020-06-23 16:00:20 -06:00
5b8d557f19 trying to break travis tests to verify 2020-06-23 15:58:55 -06:00
9a9f052c1a attempting to run tests in travis 2020-06-23 15:56:28 -06:00
Joby Elliott
45c7053db0 Update Search.php 2020-01-07 18:26:26 +00:00
Joby Elliott
770c413d7f new quote method for strings 2020-01-07 18:15:33 +00:00
Joby Elliott
94a87f91fa adding counting methods 2019-10-17 09:35:31 -06:00
Joby Elliott
d5ed2af885 Update README.md 2019-08-23 17:04:13 +00:00
Joby Elliott
39537ba978 Fix for preprocessSearch losing limit/offset 2019-04-01 16:38:58 +00:00
Joby Elliott
fd366a1297 added $sneaky arg to update()
this new argument allows update hooks to be skipped (for example, to 
avoid updating updated date for migration-type tasks)
2019-03-13 12:27:46 -06:00
Joby Elliott
a388c15650 fixing tests 2019-03-13 10:04:00 -06:00
Joby Elliott
385f6c2b89 Allow instantiation with existing PDOs 2019-03-13 09:50:51 -06:00
42 changed files with 1877 additions and 911 deletions

46
.github/workflows/test.yml vendored Normal file
View file

@ -0,0 +1,46 @@
name: Test suite
on: push
jobs:
phpunit:
name: PHPUnit
runs-on: ubuntu-latest
strategy:
matrix:
php: ["7.1", "7.2", "7.3", "7.4"]
services:
mysql:
image: mysql:5.7
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: destructr_test
ports:
- 3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
mariadb:
image: mariadb:10.2
env:
MARIADB_ROOT_PASSWORD: root
MARIADB_DATABASE: destructr_test
ports:
- 3306
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Composer install
run: composer install -o --no-progress --ignore-platform-reqs
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: dom, curl, libxml, mbstring, zip, pdo, sqlite, pdo_sqlite, mysql, mysqli, pdo_mysql
coverage: none
ini-values: variables_order=EGPCS
- name: PHPUnit
env:
TEST_MYSQL_SERVER: 127.0.0.1
TEST_MYSQL_PORT: ${{ job.services.mysql.ports['3306'] }}
TEST_MARIADB_SERVER: 127.0.0.1
TEST_MARIADB_PORT: ${{ job.services.mariadb.ports['3306'] }}
run: ./vendor/bin/phpunit

2
.gitignore vendored
View file

@ -5,3 +5,5 @@ test.php
test.sqlite
*.test.sqlite
.DS_Store
examples/example.sqlite
examples/example.sqlite-journal

View file

@ -1,28 +0,0 @@
image: php:7.1-alpine
services:
- mysql:5.7
variables:
MYSQL_DATABASE: destructr_test
MYSQL_ROOT_PASSWORD: badpassword
before_script:
- apk update
- apk add git
- docker-php-ext-install pdo
- curl -sS https://getcomposer.org/installer | php
- php composer.phar install
test:local:
script:
- php composer.phar test-local
test:mysql:
script:
- docker-php-ext-install pdo_mysql
- php composer.phar test-mysql
test:sqlite:
script:
- php composer.phar test-sqlite

View file

@ -1,5 +1,7 @@
# Destructr
[![PHPUnit Tests](https://github.com/jobyone/destructr/actions/workflows/test.yml/badge.svg)](https://github.com/jobyone/destructr/actions/workflows/test.yml)
Destructr is a specialized ORM that allows a seamless mix of structured, relational data with unstructured JSON data.
## Getting started
@ -64,19 +66,58 @@ $obj->undelete();
$obj->delete(true);
```
### Searching
Factories provide an interface for creating `Search` objects, which allow you to enter in various SQL clauses in a structured and abstract fashion.
```php
// get a new search object from the factory
$search = $factory->search();
// Search::where() takes SQL for the WHERE clause of a query
// ${path} syntax is used to reference data within objects, and
// works everywhere in searches
$search->where('${dso.date.modified} > :time');
// Search::order() takes SQL to go inside an ORDER BY clause
// in the final query.
$search->order('${dso.date.modified} desc');
// Search limit/offset methods can be used for pagination
// there is also a paginate() method for more conveniently
// paginating results
$search->paginate(20,1);
// Search::execute() returns an array of the resulting objects
$results = $search->execute();
```
## Requirements
This system relies **heavily** on the JSON features of the underlying database.
This means it cannot possibly run without a database that supports JSON features.
Exact requirements are in flux during development, but basically if a database doesn't have JSON functions it's probably impossible for Destructr to ever work with it.
Basically if a database doesn't have JSON functions it's probably impossible for Destructr to ever work with it.
At the moment there is pretty decent support for:
* MySQL >=5.7.8
* MariaDB >=10.2.7
* SQLite 3 (with some caveats)
In practice this means Destructr will **never** be able to run on less than the following versions of the following popular databases:
* MySQL >=5.7
* MariaDB >=10.2
* MySQL >=5.7.8
* MariaDB >=10.2.7
* PostgreSQL >=9.3
* SQL Server >=2016
Theoretically Destructr is also an excellent fit for NoSQL databases.
If I ever find myself needing it there's a good chance it's possible to write drivers for running it on something like MongoDB as well.
It might even be kind of easy.
### SQLite caveats
MySQL and MariaDB drivers set virtual columns to be generated automatically using their native JSON functions.
SQLite doesn't have native JSON (in most environments, at least), so Destructr itself manually updates virtual columns whenever objects are inserted or updated.
In practice this won't matter *if* you are doing all your insertion and updating via Destructr.
If you're doing updates to your database via any other method, however, you need to be aware of this, and manually update the virtual column values.

View file

@ -7,7 +7,6 @@
"prefer-stable": true,
"require": {
"php": ">=7.1",
"mofodojodino/profanity-filter": "^1.3",
"byjoby/flatrr": "^1"
},
"require-dev": {
@ -18,12 +17,12 @@
"test": [
"phpunit"
],
"test-local": [
"phpunit --testsuite Local"
],
"test-mysql": [
"phpunit --testsuite MySQL"
],
"test-mariadb": [
"phpunit --testsuite MariaDB"
],
"test-sqlite": [
"phpunit --testsuite SQLite"
]

View file

@ -0,0 +1,34 @@
<?php
use Destructr\Factory;
class ExampleFactory extends Factory
{
/**
* Example factory with a different schema, to index on random_data for faster searching
*/
protected $schema = [
'dso.id' => [
'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',
'type' => 'VARCHAR(30)',
'index' => 'BTREE',
],
'dso.deleted' => [
'name' => 'dso_deleted',
'type' => 'BIGINT',
'index' => 'BTREE',
],
'random_data' => [
'name' => 'random_data',
'type' => 'VARCHAR(64)',
'index' => 'BTREE',
],
];
}

50
examples/example_use.php Normal file
View file

@ -0,0 +1,50 @@
<?php
/**
* This file demonstrates some basic uses of Destructr, from the creation
* of a connection and factory, through to creating, inserting, updating,
* deleting, and querying data.
*/
include __DIR__ . '/../vendor/autoload.php';
/*
SQLite drivers can be created by the default factory.
A charset of UTF8 should be specified, to avoid character encoding
issues.
*/
$driver = \Destructr\DriverFactory::factory(
'sqlite:' . __DIR__ . '/example.sqlite'
);
/*
Creates a factory using the table 'example_table', and creates
the necessary table. Note that prepareEnvironment() can safely be called
multiple times. updateEnvironment shouldn't be used this way in production,
as if it is called more than once per second during a schema change, errors
may be introduced.
*/
include __DIR__ . '/example_factory.php';
$factory = new ExampleFactory($driver, 'example_table');
$factory->prepareEnvironment();
$factory->updateEnvironment();
/*
Inserting a record
*/
$obj = $factory->create(
[
'dso.type'=>'foobar',
'random_data' => md5(rand())
]
);
$obj->insert();
/*
Search by random data field, which is indexed due to the
ExampleFactory class' $schema property.
*/
$search = $factory->search();
$search->where('${random_data} LIKE :q');
$result = $search->execute(['q'=>'ab%']);
foreach($result as $r) {
var_dump($r->get());
}

View file

@ -1,15 +1,13 @@
<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="MariaDB">
<directory>tests/Drivers/MariaDB</directory>
</testsuite>
<testsuite name="SQLite">
<directory>tests/LegacyDrivers/SQLite</directory>
<directory>tests/Drivers/SQLite</directory>
</testsuite>
</testsuites>
</phpunit>

View file

@ -1,11 +1,11 @@
<?php
/* Destructr | https://gitlab.com/byjoby/destructr | MIT License */
/* Destructr | https://github.com/jobyone/destructr | MIT License */
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
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;

View file

@ -1,20 +0,0 @@
<?php
/* Destructr | https://gitlab.com/byjoby/destructr | MIT License */
namespace Destructr;
interface DSOFactoryInterface
{
public function __construct(Drivers\DSODriverInterface &$driver, string $table);
public function class(array $data) : ?string;
public function createTable() : bool;
public function create(array $data = array()) : DSOInterface;
public function read(string $value, string $field = 'dso.id', $deleted = false) : ?DSOInterface;
public function insert(DSOInterface &$dso) : bool;
public function update(DSOInterface &$dso) : bool;
public function delete(DSOInterface &$dso, bool $permanent = false) : bool;
public function search() : Search;
public function executeSearch(Search $search, array $params = array(), $deleted = false) : array;
}

View file

@ -1,5 +1,5 @@
<?php
/* Destructr | https://gitlab.com/byjoby/destructr | MIT License */
/* Destructr | https://github.com/jobyone/destructr | MIT License */
namespace Destructr;
use Flatrr\FlatArrayInterface;
@ -11,17 +11,17 @@ use Flatrr\FlatArrayInterface;
*/
interface DSOInterface extends FlatArrayInterface
{
public function __construct(array $data = null, DSOFactoryInterface &$factory = null);
public function factory(DSOFactoryInterface &$factory = null) : ?DSOFactoryInterface;
public function __construct(array $data = null, Factory $factory = null);
public function factory(Factory $factory = null): ?Factory;
public function set(string $name = null, $value, $force=false);
public function set(?string $name, $value, $force = false);
public function resetChanges();
public function changes() : array;
public function removals() : array;
public function changes(): array;
public function removals(): array;
public function insert() : bool;
public function update() : bool;
public function delete(bool $permanent = false) : bool;
public function undelete() : bool;
public function insert(): bool;
public function update(bool $sneaky = false): bool;
public function delete(bool $permanent = false): bool;
public function undelete(): bool;
}

View file

@ -1,15 +1,16 @@
<?php
/* Destructr | https://gitlab.com/byjoby/destructr | MIT License */
/* Destructr | https://github.com/jobyone/destructr | MIT License */
namespace Destructr;
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
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));
@ -21,4 +22,19 @@ class DriverFactory
return null;
}
}
public static function factoryFromPDO(\PDO $pdo, string $type = null): ?Drivers\AbstractDriver
{
if (!$type) {
$type = $pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
}
$type = strtolower($type);
if ($class = @static::$map[$type]) {
$f = new $class();
$f->pdo($pdo);
return $f;
} else {
return null;
}
}
}

View file

@ -1,120 +1,24 @@
<?php
/* Destructr | https://gitlab.com/byjoby/destructr | MIT License */
/* Destructr | https://github.com/jobyone/destructr | MIT License */
namespace Destructr\Drivers;
use Destructr\DSOInterface;
use Destructr\Search;
//TODO: Caching? It should happen somewhere in this class I think.
abstract class AbstractDriver implements DSODriverInterface
abstract class AbstractDriver
{
public $lastPreparationErrorOn;
public $pdo;
const EXTENSIBLE_VIRTUAL_COLUMNS = true;
const SCHEMA_TABLE = 'destructr_schema';
public function __construct(string $dsn, string $username=null, string $password=null, array $options=null)
{
if (!$this->pdo = new \PDO($dsn, $username, $password, $options)) {
throw new \Exception("Error creating PDO connection");
}
}
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 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;
//TODO: turn this on someday and see if caching statements helps in the real world
// $sql = $this->$fn($args);
// $id = md5($sql);
// if (!isset($this->prepared[$id])) {
// $this->prepared[$id] = $this->pdo->prepare($sql);
// }
// return @$this->prepared[$id];
}
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 beginTransaction(): bool;
abstract public function commit(): bool;
abstract public function rollBack(): bool;
abstract public function prepareEnvironment(string $table, array $schema): bool;
abstract public function updateEnvironment(string $table, array $schema): bool;
abstract public function checkEnvironment(string $table, array $schema): bool;
}

View file

@ -0,0 +1,384 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
namespace Destructr\Drivers;
use Destructr\DSOInterface;
use Destructr\Search;
use PDO;
abstract class AbstractSQLDriver extends AbstractDriver
{
public $lastPreparationErrorOn;
public $pdo;
protected $schemas = [];
protected $transactionsEnabled = true;
abstract protected function sql_ddl(array $args = []): string;
abstract protected function expandPath(string $path): string;
abstract protected function sql_set_json(array $args): string;
abstract protected function sql_insert(array $args): string;
abstract protected function sql_create_schema_table(): string;
abstract protected function sql_table_exists(string $table): string;
abstract protected function buildIndexes(string $table, array $schema): bool;
abstract protected function addColumns($table, $schema): bool;
abstract protected function removeColumns($table, $schema): bool;
abstract protected function rebuildSchema($table, $schema): bool;
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 tableExists(string $table): bool
{
try {
$stmt = $this->pdo()->prepare($this->sql_table_exists($table));
if ($stmt && $stmt->execute() !== false) {
return true;
} else {
return false;
}
} catch (\Throwable $th) {
return false;
}
}
public function createSchemaTable()
{
try {
$this->pdo->exec($this->sql_create_schema_table());
} catch (\Throwable $th) {
}
return $this->tableExists(AbstractDriver::SCHEMA_TABLE);
}
public function pdo(\PDO $pdo = null): ?\PDO
{
if ($pdo) {
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$this->pdo = $pdo;
}
return $this->pdo;
}
public function disableTransactions()
{
$this->transactionsEnabled = false;
}
public function enableTransactions()
{
$this->transactionsEnabled = true;
}
public function beginTransaction(): bool
{
if (!$this->transactionsEnabled) return true;
return $this->pdo->beginTransaction();
}
public function commit(): bool
{
if (!$this->transactionsEnabled) return true;
return $this->pdo->commit();
}
public function rollBack(): bool
{
if (!$this->transactionsEnabled) return true;
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;
}
}
public function checkEnvironment(string $table, array $schema): bool
{
return $this->tableExists(AbstractDriver::SCHEMA_TABLE) && $this->getSchema($table) == $schema;
}
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
$tableExists = $this->tableExists($table);
// if table exists, but no schema does, assume table matches schema and save schema
if ($tableExists && !$this->getSchema($table)) {
$this->saveSchema($table, $schema);
return true;
}
// 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 (!$tableExists) {
$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 <<<EOT
INSERT INTO `destructr_schema`
(schema_time,schema_table,schema_schema)
VALUES ($time,$table,$schema);
EOT;
}
protected function sql_get_schema(array $args)
{
return <<<EOT
SELECT * FROM `destructr_schema`
WHERE `schema_table` = :table
ORDER BY `schema_time` desc
LIMIT 1
EOT;
}
public function update(string $table, DSOInterface $dso): bool
{
if (!$dso->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;';
}
}

View file

@ -1,19 +0,0 @@
<?php
/* Destructr | https://gitlab.com/byjoby/destructr | MIT License */
namespace Destructr\Drivers;
use Destructr\DSOInterface;
use Destructr\Search;
interface DSODriverInterface
{
public function __construct(string $dsn, string $username=null, string $password=null, array $options=null);
public function createTable(string $table, array $virtualColumns) : bool;
public function select(string $table, Search $search, array $params);
public function insert(string $table, DSOInterface $dso) : bool;
public function update(string $table, DSOInterface $dso) : bool;
public function delete(string $table, DSOInterface $dso) : bool;
public function errorInfo();
}

View file

@ -0,0 +1,65 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
namespace Destructr\Drivers;
/**
* What this driver supports: MariaDB >= 10.2.7
*/
class MariaDBDriver extends MySQLDriver
{
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['schema'] as $path => $col) {
$line = "`{$col['name']}` {$col['type']} GENERATED ALWAYS AS (" . $this->expandPath($path) . ") VIRTUAL";
$lines[] = $line;
}
$out[] = implode(',' . PHP_EOL, $lines);
$out[] = ") ENGINE=InnoDB DEFAULT CHARSET=utf8;";
$out = implode(PHP_EOL, $out);
return $out;
}
protected function buildIndexes(string $table, array $schema): bool
{
foreach ($schema as $path => $col) {
try {
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"
);
}
} catch (\Throwable $th) {
}
}
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;
}
}

View file

@ -1,97 +1,119 @@
<?php
/* Destructr | https://gitlab.com/byjoby/destructr | MIT License */
/* Destructr | https://github.com/jobyone/destructr | MIT License */
namespace Destructr\Drivers;
/**
* What this driver supports: MySQL and MariaDB databases new enough to support
* JSON functions. This means:
* * MySQL >= 5.7
* * MariaDB >= 10.2
* 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($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())
protected function sql_ddl(array $args = []): string
{
$out = [];
$out[] = "CREATE TABLE `{$args['table']}` (";
$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).")";
foreach ($args['schema'] as $path => $col) {
$line = "`{$col['name']}` {$col['type']} GENERATED ALWAYS AS (" . $this->expandPath($path) . ")";
if (@$col['primary']) {
//this needs to be "PERSISTENT" for MariaDB -- I guess there are going to be two drivers now
$line .= ' STORED';
} else {
$line .= ' VIRTUAL';
}
$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[] = implode(',' . PHP_EOL, $lines);
$out[] = ") ENGINE=InnoDB DEFAULT CHARSET=utf8;";
return implode(PHP_EOL, $out);
$out = implode(PHP_EOL, $out);
return $out;
}
protected function expandPath(string $path) : string
protected function buildIndexes(string $table, array $schema): bool
{
foreach ($schema as $path => $col) {
try {
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"
);
}
} catch (\Throwable $th) {
}
}
return true;
}
protected function expandPath(string $path): string
{
return "JSON_UNQUOTE(JSON_EXTRACT(`json_data`,'$.{$path}'))";
}
protected function sql_setJSON($args)
protected function sql_set_json(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 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 <<<EOT
CREATE TABLE `destructr_schema` (
`schema_time` bigint NOT NULL,
`schema_table` varchar(100) NOT NULL,
`schema_schema` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL CHECK (json_valid(`schema_schema`)),
PRIMARY KEY (`schema_time`,`schema_table`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
EOT;
}
protected function sql_table_exists(string $table): string
{
$table = preg_replace('/[^a-zA-Z0-9\-_]/', '', $table);
return 'SELECT 1 FROM ' . $table . ' LIMIT 1';
}
}

View file

@ -0,0 +1,272 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
namespace Destructr\Drivers;
use Destructr\DSOInterface;
/**
* 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 cognizant of if your
* data is being updated outside Destructr.
*/
class SQLiteDriver extends AbstractSQLDriver
{
public function update(string $table, DSOInterface $dso): bool
{
if (!$dso->changes() && !$dso->removals()) {
return true;
}
$columns = $this->dso_columns($dso);
$s = $this->getStatement(
'set_json',
[
'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 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 = [];
$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_set_json(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'],
]);
}
/**
* 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' => json_encode($dso->get())];
foreach ($this->getSchema($dso->factory()->table()) ?? [] 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";
}
protected function buildIndexes(string $table, array $schema): bool
{
$result = true;
foreach ($schema as $key => $vcol) {
try {
if (@$vcol['primary']) {
//sqlite automatically creates this index
} elseif (@$vcol['unique']) {
$result = $result &&
$this->pdo->exec('CREATE UNIQUE INDEX ' . $table . '_' . $vcol['name'] . '_idx on `' . $table . '`(`' . $vcol['name'] . '`)') !== false;
} elseif (@$vcol['index']) {
$idxResult = $result &&
$this->pdo->exec('CREATE INDEX ' . $table . '_' . $vcol['name'] . '_idx on `' . $table . '`(`' . $vcol['name'] . '`)') !== false;
}
} catch (\Throwable $th) {
}
}
return $result;
}
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['schema'] 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}')";
}
protected function sql_create_schema_table(): string
{
return <<<EOT
CREATE TABLE IF NOT EXISTS `destructr_schema`(
schema_time BIGINT NOT NULL,
schema_table VARCHAR(100) NOT NULL,
schema_schema TEXT NOT NULL
);
EOT;
}
protected function sql_table_exists(string $table): string
{
$table = preg_replace('/[^a-zA-Z0-9\-_]/', '', $table);
return 'SELECT 1 FROM ' . $table . ' LIMIT 1';
}
}

View file

@ -1,8 +1,9 @@
<?php
/* Destructr | https://gitlab.com/byjoby/destructr | MIT License */
/* Destructr | https://github.com/jobyone/destructr | MIT License */
namespace Destructr;
use mofodojodino\ProfanityFilter\Check;
use Destructr\Drivers\AbstractDriver;
/**
* The Factory is responsible for keeping track of which columns may or may not
@ -19,67 +20,100 @@ use mofodojodino\ProfanityFilter\Check;
* * Executing Searches (which largely consists of passing them to the Driver)
* * Inspecting unstructured data straight from the database and figuring out what class to make it (defaults to just DSO)
*/
class Factory implements DSOFactoryInterface
class Factory
{
const ID_CHARS = 'abcdefghijkmnorstuvwxyz0123456789';
const ID_LENGTH = 16;
const ID_CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789';
const ID_LENGTH = 8;
/**
* @var Drivers\AbstractDriver
*/
protected $driver;
/**
* @var string
*/
protected $table;
/**
* 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.
* Virtual columns that should be created for sorting/indexing in the SQL server
*/
protected $virtualColumns = [
protected $schema = [
'dso.id' => [
'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',
'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)
public function __construct(Drivers\AbstractDriver $driver, string $table)
{
$this->driver = $driver;
$this->table = $table;
}
protected function hook_create(DSOInterface &$dso)
public function checkEnvironment(): bool
{
return $this->driver->checkEnvironment(
$this->table,
$this->schema
);
}
public function prepareEnvironment(): bool
{
return $this->driver->prepareEnvironment(
$this->table,
$this->schema
);
}
public function updateEnvironment(): bool
{
return $this->driver->updateEnvironment(
$this->table,
$this->schema
);
}
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(AbstractDriver::SCHEMA_TABLE);
return $this->driver->tableExists(AbstractDriver::SCHEMA_TABLE);
}
public function quote(string $str): string
{
return $this->driver->pdo()->quote($str);
}
protected function hook_create(DSOInterface $dso)
{
if (!$dso->get('dso.id')) {
$dso->set('dso.id', static::generate_id(static::ID_CHARS, static::ID_LENGTH), true);
@ -88,37 +122,46 @@ 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)
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']]);
}
public function class(array $data) : ?string
/**
* Override this function to allow a factory to create different
* sub-classes of DSO based on attributes of the given object's
* data. For example, you could use a property like dso.class to
* select a class from an associative array.
*
* @param array $data
* @return string|null
*/
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);
}
$dso['dso.deleted'] = time();
return $this->update($dso);
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);
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;
@ -130,37 +173,31 @@ class Factory implements DSOFactoryInterface
return $dso;
}
public function createTable() : bool
public function schema(): array
{
return $this->driver->createTable(
$this->table,
($this->driver::EXTENSIBLE_VIRTUAL_COLUMNS?$this->virtualColumns:$this::CORE_VIRTUAL_COLUMNS)
);
return $this->driver->getSchema($this->table) ?? $this->schema;
}
protected function virtualColumnName($path) : ?string
protected function virtualColumnName($path): ?string
{
if ($this->driver::EXTENSIBLE_VIRTUAL_COLUMNS) {
$vcols = $this->virtualColumns;
} else {
$vcols = static::CORE_VIRTUAL_COLUMNS;
}
return @$vcols[$path]['name'];
return @$this->schema()[$path]['name'];
}
public function update(DSOInterface &$dso) : bool
public function update(DSOInterface $dso, bool $sneaky = false): bool
{
if (!$dso->changes() && !$dso->removals()) {
return true;
}
$this->hook_update($dso);
$dso->hook_update();
if (!$sneaky) {
$this->hook_update($dso);
$dso->hook_update();
}
$out = $this->driver->update($this->table, $dso);
$dso->resetChanges();
return $out;
}
public function search() : Search
public function search(): Search
{
return new Search($this);
}
@ -179,7 +216,19 @@ class Factory implements DSOFactoryInterface
return $arr;
}
public function executeSearch(Search $search, array $params = array(), $deleted = false) : array
public function executeCount(Search $search, array $params = array(), $deleted = false): ?int
{
//add deletion clause and expand column names
$search = $this->preprocessSearch($search, $deleted);
//run select
return $this->driver->count(
$this->table,
$search,
$params
);
}
public function executeSearch(Search $search, array $params = array(), $deleted = false): array
{
//add deletion clause and expand column names
$search = $this->preprocessSearch($search, $deleted);
@ -192,17 +241,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();
@ -216,6 +265,8 @@ class Factory implements DSOFactoryInterface
$search = new Search($this);
$search->where($input->where());
$search->order($input->order());
$search->limit($input->limit());
$search->offset($input->offset());
/* add deletion awareness to where clause */
if ($deleted !== null) {
$where = $search->where();
@ -225,14 +276,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(
'/\$\{([^\}\\\]+)\}/',
@ -252,19 +303,17 @@ class Factory implements DSOFactoryInterface
return $search;
}
protected static function generate_id($chars, $length) : string
protected static function generate_id($chars, $length, string $prefix = null): string
{
$check = new Check();
do {
$id = '';
while (strlen($id) < $length) {
$id .= substr(
$chars,
rand(0, strlen($chars)-1),
1
);
}
} while ($check->hasProfanity($id));
$id = '';
while (strlen($id) < $length) {
$id .= substr(
$chars,
rand(0, strlen($chars) - 1),
1
);
}
if ($prefix) $id = $prefix . '_' . $id;
return $id;
}

View file

@ -1,183 +0,0 @@
<?php
/* Destructr | https://gitlab.com/byjoby/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_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,97 +0,0 @@
<?php
/* Destructr | https://gitlab.com/byjoby/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 __construct(string $dsn, string $username=null, string $password=null, array $options=null)
{
parent::__construct($dsn, $username, $password, $options);
/*
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
);
}
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 `{$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

@ -1,10 +1,7 @@
<?php
/* Destructr | https://gitlab.com/byjoby/destructr | MIT License */
/* Destructr | https://github.com/jobyone/destructr | MIT License */
namespace Destructr;
use Destructr\DSOFactoryInterface;
use Destructr\Drivers\DSODriverInterface;
class Search implements \Serializable
{
protected $factory;
@ -13,17 +10,27 @@ class Search implements \Serializable
protected $limit;
protected $offset;
public function __construct(DSOFactoryInterface &$factory=null)
public function __construct(Factory $factory = null)
{
$this->factory = $factory;
}
public function quote(string $str): string
{
return $this->factory->quote($str);
}
public function count(array $params = array(), $deleted = false)
{
return $this->factory->executeCount($this, $params, $deleted);
}
public function execute(array $params = array(), $deleted = false)
{
return $this->factory->executeSearch($this, $params, $deleted);
}
public function where(string $set = null) : ?string
public function where(string $set = null): ?string
{
if ($set !== null) {
$this->where = $set;
@ -31,7 +38,18 @@ class Search implements \Serializable
return $this->where;
}
public function order(string $set = null) : ?string
public function paginate(int $perPage, int $page = 1)
{
$this->limit($perPage);
$this->offset(($page - 1) * $perPage);
}
public function pageCount(int $perPage)
{
return ceil($this->count() / $perPage);
}
public function order(string $set = null): ?string
{
if ($set !== null) {
$this->order = $set;
@ -39,7 +57,7 @@ class Search implements \Serializable
return $this->order;
}
public function limit(int $set = null) : ?int
public function limit(int $set = null): ?int
{
if ($set !== null) {
$this->limit = $set;
@ -47,7 +65,7 @@ class Search implements \Serializable
return $this->limit;
}
public function offset(int $set = null) : ?int
public function offset(int $set = null): ?int
{
if ($set !== null) {
$this->offset = $set;
@ -58,7 +76,7 @@ class Search implements \Serializable
public function serialize()
{
return json_encode(
[$this->where(),$this->order(),$this->limit(),$this->offset()]
[$this->where(), $this->order(), $this->limit(), $this->offset()]
);
}

View file

@ -1,5 +1,5 @@
<?php
/* Destructr | https://gitlab.com/byjoby/destructr | MIT License */
/* Destructr | https://github.com/jobyone/destructr | MIT License */
declare(strict_types=1);
namespace Destructr;

View file

@ -1,35 +1,42 @@
<?php
/* Destructr | https://gitlab.com/byjoby/destructr | MIT License */
declare(strict_types=1);
/* Destructr | https://github.com/jobyone/destructr | MIT License */
declare (strict_types = 1);
namespace Destructr\Drivers;
use PHPUnit\Framework\TestCase;
use PHPUnit\DbUnit\TestCaseTrait;
use Destructr\DSO;
use Destructr\Search;
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';
protected static function DRIVER_USERNAME()
{
return null;
}
protected static function DRIVER_PASSWORD()
{
return null;
}
protected static function DRIVER_OPTIONS()
{
return null;
}
public static function setUpBeforeClass()
{
$pdo = static::createPDO();
$pdo->exec('DROP TABLE '.static::TEST_TABLE);
$pdo->exec('DROP TABLE ' . static::TEST_TABLE);
}
public function testCreateTable()
public function testPrepareEnvironment()
{
$factory = $this->createFactory();
//should work the first time
$out = $factory->createTable();
if (!$out) {
var_dump($factory->errorInfo());
}
$this->assertTrue($out);
//but not the second time, because it already exists
$this->assertFalse($factory->createTable());
$factory->prepareEnvironment();
//table should exist and have zero rows
$this->assertEquals(0, $this->getConnection()->getRowCount(static::TEST_TABLE));
}
@ -39,12 +46,12 @@ abstract class AbstractDriverIntegrationTest extends TestCase
$startRowCount = $this->getConnection()->getRowCount(static::TEST_TABLE);
$factory = $this->createFactory();
//inserting a freshly created object should return true
$o = $factory->create(['dso.id'=>'object-one']);
$o = $factory->create(['dso.id' => 'object-one']);
$this->assertTrue($o->insert());
//inserting it a second time should not
$this->assertFalse($o->insert());
//there should now be one more row
$this->assertEquals($startRowCount+1, $this->getConnection()->getRowCount(static::TEST_TABLE));
$this->assertEquals($startRowCount + 1, $this->getConnection()->getRowCount(static::TEST_TABLE));
}
public function testReadAndUpdate()
@ -52,9 +59,9 @@ abstract class AbstractDriverIntegrationTest extends TestCase
$startRowCount = $this->getConnection()->getRowCount(static::TEST_TABLE);
$factory = $this->createFactory();
//insert some new objects
$a1 = $factory->create(['foo'=>'bar']);
$a1 = $factory->create(['foo' => 'bar']);
$a1->insert();
$b1 = $factory->create(['foo.bar'=>'baz']);
$b1 = $factory->create(['foo.bar' => 'baz']);
$b1->insert();
//read objects back out
$a2 = $factory->read($a1['dso.id']);
@ -77,7 +84,7 @@ abstract class AbstractDriverIntegrationTest extends TestCase
$this->assertNotEquals($a1->get(), $a3->get());
$this->assertNotEquals($b1->get(), $b3->get());
//there should now be two more rows
$this->assertEquals($startRowCount+2, $this->getConnection()->getRowCount(static::TEST_TABLE));
$this->assertEquals($startRowCount + 2, $this->getConnection()->getRowCount(static::TEST_TABLE));
}
public function testDelete()
@ -85,17 +92,17 @@ abstract class AbstractDriverIntegrationTest extends TestCase
$startRowCount = $this->getConnection()->getRowCount(static::TEST_TABLE);
$factory = $this->createFactory();
//insert some new objects
$a1 = $factory->create(['testDelete'=>'undelete me']);
$a1 = $factory->create(['testDelete' => 'undelete me']);
$a1->insert();
$b1 = $factory->create(['testDelete'=>'should be permanently deleted']);
$b1 = $factory->create(['testDelete' => 'should be permanently deleted']);
$b1->insert();
//there should now be two more rows
$this->assertEquals($startRowCount+2, $this->getConnection()->getRowCount(static::TEST_TABLE));
$this->assertEquals($startRowCount + 2, $this->getConnection()->getRowCount(static::TEST_TABLE));
//delete one permanently and the other not, both shoudl take effect immediately
$a1->delete();
$b1->delete(true);
//there should now be only one more row
$this->assertEquals($startRowCount+1, $this->getConnection()->getRowCount(static::TEST_TABLE));
$this->assertEquals($startRowCount + 1, $this->getConnection()->getRowCount(static::TEST_TABLE));
//a should be possible to read a back out with the right flags
$this->assertNull($factory->read($a1['dso.id']));
$this->assertNotNull($factory->read($a1['dso.id'], 'dso.id', true));
@ -116,25 +123,25 @@ abstract class AbstractDriverIntegrationTest extends TestCase
$factory->create([
'testSearch' => 'a',
'a' => '1',
'b' => '2'
'b' => '2',
])->insert();
$factory->create([
'testSearch' => 'b',
'a' => '2',
'b' => '1'
'b' => '1',
])->insert();
$factory->create([
'testSearch' => 'c',
'a' => '3',
'b' => '4'
'b' => '4',
])->insert();
$factory->create([
'testSearch' => 'a',
'a' => '4',
'b' => '3'
'b' => '3',
])->insert();
//there should now be four more rows
$this->assertEquals($startRowCount+4, $this->getConnection()->getRowCount(static::TEST_TABLE));
$this->assertEquals($startRowCount + 4, $this->getConnection()->getRowCount(static::TEST_TABLE));
//TODO: test some searches
}
@ -146,10 +153,10 @@ abstract class AbstractDriverIntegrationTest extends TestCase
{
$class = static::DRIVER_CLASS;
return new $class(
static::DRIVER_DSN,
static::DRIVER_USERNAME,
static::DRIVER_PASSWORD,
static::DRIVER_OPTIONS
static::DRIVER_DSN(),
static::DRIVER_USERNAME(),
static::DRIVER_PASSWORD(),
static::DRIVER_OPTIONS()
);
}
@ -165,10 +172,10 @@ abstract class AbstractDriverIntegrationTest extends TestCase
protected static function createPDO()
{
return new \PDO(
static::DRIVER_DSN,
static::DRIVER_USERNAME,
static::DRIVER_PASSWORD,
static::DRIVER_OPTIONS
static::DRIVER_DSN(),
static::DRIVER_USERNAME(),
static::DRIVER_PASSWORD(),
static::DRIVER_OPTIONS()
);
}

View file

@ -0,0 +1,154 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
declare (strict_types = 1);
namespace Destructr\Drivers;
use PDO;
use PHPUnit\DbUnit\TestCaseTrait;
use PHPUnit\Framework\TestCase;
/**
* This class tests a Driver's ability to correctly change schemas.
*/
abstract class AbstractSQLDriverSchemaChangeTest extends TestCase
{
use TestCaseTrait;
const TEST_TABLE = 'schematest';
protected static function DRIVER_USERNAME()
{
return null;
}
protected static function DRIVER_PASSWORD()
{
return null;
}
protected static function DRIVER_OPTIONS()
{
return null;
}
public function testSchemaChanges()
{
// set up using schema A
$factory = $this->createFactoryA();
$this->assertFalse($factory->checkEnvironment());
$factory->prepareEnvironment();
$this->assertTrue($factory->checkEnvironment());
$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();
$this->assertFalse($factory->checkEnvironment());
$factory->prepareEnvironment();
$this->assertFalse($factory->checkEnvironment());
$factory->updateEnvironment();
$this->assertTrue($factory->checkEnvironment());
// 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');
}
}

View file

@ -1,78 +1,97 @@
<?php
/* Destructr | https://gitlab.com/byjoby/destructr | MIT License */
/* Destructr | https://github.com/jobyone/destructr | MIT License */
declare(strict_types=1);
namespace Destructr\Drivers;
use PHPUnit\Framework\TestCase;
use PHPUnit\DbUnit\TestCaseTrait;
use Destructr\DSO;
use Destructr\Factory;
use Destructr\Search;
use PHPUnit\DbUnit\TestCaseTrait;
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;
abstract protected static function DRIVER_DSN();
protected static function DRIVER_USERNAME()
{
return null;
}
protected static function DRIVER_PASSWORD()
{
return null;
}
protected static function DRIVER_OPTIONS()
{
return null;
}
/*
In actual practice, these would come from a Factory
*/
protected $virtualColumns = [
protected $schema = [
'dso.id' => [
'name'=>'dso_id',
'type'=>'VARCHAR(16)',
'name' => 'dso_id',
'type' => 'VARCHAR(16)',
'index' => 'BTREE',
'unique' => true
'unique' => true,
],
'dso.type' => [
'name'=>'dso_type',
'type'=>'VARCHAR(30)',
'index'=>'BTREE'
'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 testCreateTable()
public function testPrepareEnvironment()
{
$driver = $this->createDriver();
$res = $driver->createTable('testCreateTable', $this->virtualColumns);
$this->assertTrue($res);
$this->assertEquals(0, $this->getConnection()->getRowCount('testCreateTable'));
$this->assertFalse($driver->createTable('testCreateTable', $this->virtualColumns));
$this->assertFalse($driver->tableExists('testPrepareEnvironment'));
$this->assertFalse($driver->tableExists(AbstractDriver::SCHEMA_TABLE));
$driver->prepareEnvironment('testPrepareEnvironment', $this->schema);
$this->assertTrue($driver->tableExists(AbstractDriver::SCHEMA_TABLE));
$this->assertTrue($driver->tableExists('testPrepareEnvironment'));
$this->assertEquals(1, $this->getConnection()->getRowCount(AbstractDriver::SCHEMA_TABLE));
$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']);
$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']);
$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
@ -97,79 +116,38 @@ abstract class AbstractDriverTest extends TestCase
// search with no results, searching by virtual column
$search = new Search();
$search->where('`dso_type` = :param');
$results = $driver->select('testSelect', $search, [':param'=>'type-none']);
$results = $driver->select('testSelect', $search, [':param' => 'type-none']);
$this->assertSame(0, count($results));
// search with no results, searching by json field
$search = new Search();
$search->where('${foo} = :param');
$results = $driver->select('testSelect', $search, [':param'=>'nonexistent foo value']);
$results = $driver->select('testSelect', $search, [':param' => 'nonexistent foo value']);
$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']);
$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']);
$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'
]));
$driver->insert('testDelete', new DSO([
'dso'=>['id'=>'item-a-2','type'=>'type-a'],
'foo'=>'baz',
'sort'=>'c'
]));
$driver->insert('testDelete', new DSO([
'dso'=>['id'=>'item-b-1','type'=>'type-b'],
'foo'=>'buz',
'sort'=>'b'
]));
$driver->insert('testDelete', new DSO([
'dso'=>['id'=>'item-b-2','type'=>'type-b','deleted'=>100],
'foo'=>'quz',
'sort'=>'d'
]));
}
protected function setup_testSelect()
{
$driver = $this->createDriver();
$driver->insert('testSelect', new DSO([
'dso'=>['id'=>'item-a-1','type'=>'type-a'],
'foo'=>'bar',
'sort'=>'a'
]));
'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'
]));
'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'
]));
'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'
]));
'dso' => ['id' => 'item-b-2', 'type' => 'type-b', 'deleted' => 100],
'foo' => 'quz',
'sort' => 'd',
], new Factory($driver, 'no_table')));
}
/**
@ -180,29 +158,29 @@ abstract class AbstractDriverTest extends TestCase
{
$class = static::DRIVER_CLASS;
return new $class(
static::DRIVER_DSN,
static::DRIVER_USERNAME,
static::DRIVER_PASSWORD,
static::DRIVER_OPTIONS
static::DRIVER_DSN(),
static::DRIVER_USERNAME(),
static::DRIVER_PASSWORD(),
static::DRIVER_OPTIONS()
);
}
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()
{
return new \PDO(
static::DRIVER_DSN,
static::DRIVER_USERNAME,
static::DRIVER_PASSWORD,
static::DRIVER_OPTIONS
static::DRIVER_DSN(),
static::DRIVER_USERNAME(),
static::DRIVER_PASSWORD(),
static::DRIVER_OPTIONS()
);
}

View file

@ -0,0 +1,30 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
declare (strict_types = 1);
namespace Destructr\Drivers;
use Destructr\Factory;
class FactorySchemaA extends Factory
{
public $schema = [
'dso.id' => [
'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',
],
];
}

View file

@ -0,0 +1,30 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
declare (strict_types = 1);
namespace Destructr\Drivers;
use Destructr\Factory;
class FactorySchemaB extends Factory
{
public $schema = [
'dso.id' => [
'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',
],
];
}

View file

@ -0,0 +1,39 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
declare(strict_types=1);
namespace Destructr\Drivers\MariaDB;
use Destructr\Drivers\AbstractSQLDriverIntegrationTest;
use Destructr\Drivers\MariaDBDriver;
class MariaDBDriverIntegrationTest extends AbstractSQLDriverIntegrationTest
{
const DRIVER_CLASS = MariaDBDriver::class;
protected static function DRIVER_DSN()
{
return sprintf(
'mysql:host=%s:%s;dbname=%s',
$_ENV['TEST_MYSQL_SERVER'],
$_ENV['TEST_MYSQL_PORT'],
static::DRIVER_DBNAME()
);
}
protected static function DRIVER_DBNAME()
{
return @$_ENV['TEST_MARIADB_DBNAME'] ?? 'destructr_test';
}
protected static function DRIVER_USERNAME()
{
return @$_ENV['TEST_MARIADB_USER'] ?? 'root';
}
protected static function DRIVER_PASSWORD()
{
return @$_ENV['TEST_MARIADB_PASSWORD'] ?? 'root';
}
}

View file

@ -0,0 +1,39 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
declare(strict_types=1);
namespace Destructr\Drivers\MariaDB;
use Destructr\Drivers\AbstractSQLDriverSchemaChangeTest;
use Destructr\Drivers\MariaDBDriver;
class MariaDBDriverSchemaChangeTest extends AbstractSQLDriverSchemaChangeTest
{
const DRIVER_CLASS = MariaDBDriver::class;
protected static function DRIVER_DSN()
{
return sprintf(
'mysql:host=%s:%s;dbname=%s',
$_ENV['TEST_MYSQL_SERVER'],
$_ENV['TEST_MYSQL_PORT'],
static::DRIVER_DBNAME()
);
}
protected static function DRIVER_DBNAME()
{
return @$_ENV['TEST_MARIADB_DBNAME'] ?? 'destructr_test';
}
protected static function DRIVER_USERNAME()
{
return @$_ENV['TEST_MARIADB_USER'] ?? 'root';
}
protected static function DRIVER_PASSWORD()
{
return @$_ENV['TEST_MARIADB_PASSWORD'] ?? 'root';
}
}

View file

@ -0,0 +1,39 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
declare(strict_types=1);
namespace Destructr\Drivers\MariaDB;
use Destructr\Drivers\AbstractSQLDriverTest;
use Destructr\Drivers\MariaDBDriver;
class MariaDBDriverTest extends AbstractSQLDriverTest
{
const DRIVER_CLASS = MariaDBDriver::class;
protected static function DRIVER_DSN()
{
return sprintf(
'mysql:host=%s:%s;dbname=%s',
$_ENV['TEST_MYSQL_SERVER'],
$_ENV['TEST_MYSQL_PORT'],
static::DRIVER_DBNAME()
);
}
protected static function DRIVER_DBNAME()
{
return @$_ENV['TEST_MARIADB_DBNAME'] ?? 'destructr_test';
}
protected static function DRIVER_USERNAME()
{
return @$_ENV['TEST_MARIADB_USER'] ?? 'root';
}
protected static function DRIVER_PASSWORD()
{
return @$_ENV['TEST_MARIADB_PASSWORD'] ?? 'root';
}
}

View file

@ -1,18 +1,39 @@
<?php
/* Destructr | https://gitlab.com/byjoby/destructr | MIT License */
/* Destructr | https://github.com/jobyone/destructr | MIT License */
declare(strict_types=1);
namespace Destructr\Drivers\MySQL;
use PHPUnit\Framework\TestCase;
use Destructr\Drivers\AbstractDriverIntegrationTest;
use Destructr\Drivers\AbstractSQLDriverIntegrationTest;
use Destructr\Drivers\MySQLDriver;
class MySQLDriverIntegrationTest extends AbstractDriverIntegrationTest
class MySQLDriverIntegrationTest extends AbstractSQLDriverIntegrationTest
{
const DRIVER_CLASS = \Destructr\Drivers\MySQLDriver::class;
const DRIVER_DSN = 'mysql:host=mysql;dbname=destructr_test';
const DRIVER_USERNAME = 'root';
const DRIVER_PASSWORD = 'badpassword';
const DRIVER_OPTIONS = null;
const TEST_TABLE = 'integrationtest';
const DRIVER_CLASS = MySQLDriver::class;
protected static function DRIVER_DSN()
{
return sprintf(
'mysql:host=%s:%s;dbname=%s',
$_ENV['TEST_MYSQL_SERVER'],
$_ENV['TEST_MYSQL_PORT'],
static::DRIVER_DBNAME()
);
}
protected static function DRIVER_DBNAME()
{
return @$_ENV['TEST_MYSQL_DBNAME'] ?? 'destructr_test';
}
protected static function DRIVER_USERNAME()
{
return @$_ENV['TEST_MYSQL_USER'] ?? 'root';
}
protected static function DRIVER_PASSWORD()
{
return @$_ENV['TEST_MYSQL_PASSWORD'] ?? 'root';
}
}

View file

@ -0,0 +1,39 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
declare(strict_types=1);
namespace Destructr\Drivers\MySQL;
use Destructr\Drivers\AbstractSQLDriverSchemaChangeTest;
use Destructr\Drivers\MySQLDriver;
class MySQLDriverSchemaChangeTest extends AbstractSQLDriverSchemaChangeTest
{
const DRIVER_CLASS = MySQLDriver::class;
protected static function DRIVER_DSN()
{
return sprintf(
'mysql:host=%s:%s;dbname=%s',
$_ENV['TEST_MYSQL_SERVER'],
$_ENV['TEST_MYSQL_PORT'],
static::DRIVER_DBNAME()
);
}
protected static function DRIVER_DBNAME()
{
return @$_ENV['TEST_MYSQL_DBNAME'] ?? 'destructr_test';
}
protected static function DRIVER_USERNAME()
{
return @$_ENV['TEST_MYSQL_USER'] ?? 'root';
}
protected static function DRIVER_PASSWORD()
{
return @$_ENV['TEST_MYSQL_PASSWORD'] ?? 'root';
}
}

View file

@ -1,17 +1,39 @@
<?php
/* Destructr | https://gitlab.com/byjoby/destructr | MIT License */
/* Destructr | https://github.com/jobyone/destructr | MIT License */
declare(strict_types=1);
namespace Destructr\Drivers\MySQL;
use PHPUnit\Framework\TestCase;
use Destructr\Drivers\AbstractDriverTest;
use Destructr\Drivers\AbstractSQLDriverTest;
use Destructr\Drivers\MySQLDriver;
class MySQLDriverTest extends AbstractDriverTest
class MySQLDriverTest extends AbstractSQLDriverTest
{
const DRIVER_CLASS = MySQLDriver::class;
const DRIVER_DSN = 'mysql:host=mysql;dbname=destructr_test';
const DRIVER_USERNAME = 'root';
const DRIVER_PASSWORD = 'badpassword';
const DRIVER_OPTIONS = null;
protected static function DRIVER_DSN()
{
return sprintf(
'mysql:host=%s:%s;dbname=%s',
$_ENV['TEST_MYSQL_SERVER'],
$_ENV['TEST_MYSQL_PORT'],
static::DRIVER_DBNAME()
);
}
protected static function DRIVER_DBNAME()
{
return @$_ENV['TEST_MYSQL_DBNAME'] ?? 'destructr_test';
}
protected static function DRIVER_USERNAME()
{
return @$_ENV['TEST_MYSQL_USER'] ?? 'root';
}
protected static function DRIVER_PASSWORD()
{
return @$_ENV['TEST_MYSQL_PASSWORD'] ?? 'root';
}
}

View file

@ -0,0 +1,34 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
declare(strict_types=1);
namespace Destructr\Drivers\SQLite;
use Destructr\Drivers\AbstractSQLDriverIntegrationTest;
use Destructr\Drivers\SQLiteDriver;
class SQLiteDriverIntegrationTest extends AbstractSQLDriverIntegrationTest
{
const DRIVER_CLASS = SQLiteDriver::class;
public static function setUpBeforeClass()
{
@unlink(__DIR__ . '/integration.test.sqlite');
}
public static function DRIVER_DSN()
{
return 'sqlite:' . __DIR__ . '/integration.test.sqlite';
}
protected static function DRIVER_DBNAME()
{
return null;
}
protected static function DRIVER_USERNAME()
{
return null;
}
}

View file

@ -0,0 +1,34 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
declare(strict_types=1);
namespace Destructr\Drivers\SQLite;
use Destructr\Drivers\AbstractSQLDriverSchemaChangeTest;
use Destructr\Drivers\SQLiteDriver;
class SQLiteDriverSchemaChangeTest extends AbstractSQLDriverSchemaChangeTest
{
const DRIVER_CLASS = SQLiteDriver::class;
public static function setUpBeforeClass()
{
@unlink(__DIR__ . '/schema.test.sqlite');
}
public static function DRIVER_DSN()
{
return 'sqlite:' . __DIR__ . '/schema.test.sqlite';
}
protected static function DRIVER_DBNAME()
{
return null;
}
protected static function DRIVER_USERNAME()
{
return null;
}
}

View file

@ -0,0 +1,34 @@
<?php
/* Destructr | https://github.com/jobyone/destructr | MIT License */
declare(strict_types=1);
namespace Destructr\Drivers\SQLite;
use Destructr\Drivers\AbstractSQLDriverTest;
use Destructr\Drivers\SQLiteDriver;
class SQLiteDriverTest extends AbstractSQLDriverTest
{
const DRIVER_CLASS = SQLiteDriver::class;
public static function setUpBeforeClass()
{
@unlink(__DIR__ . '/driver.test.sqlite');
}
public static function DRIVER_DSN()
{
return 'sqlite:' . __DIR__ . '/driver.test.sqlite';
}
protected static function DRIVER_DBNAME()
{
return null;
}
protected static function DRIVER_USERNAME()
{
return null;
}
}

View file

@ -1,5 +1,5 @@
<?php
/* Destructr | https://gitlab.com/byjoby/destructr | MIT License */
/* Destructr | https://github.com/jobyone/destructr | MIT License */
declare(strict_types=1);
namespace Destructr;

View file

@ -1,8 +1,8 @@
<?php
/* Destructr | https://gitlab.com/byjoby/destructr | MIT License */
/* Destructr | https://github.com/jobyone/destructr | MIT License */
namespace Destructr;
class HarnessDriver implements Drivers\DSODriverInterface
class HarnessDriver extends Drivers\AbstractDriver
{
const EXTENSIBLE_VIRTUAL_COLUMNS = true;
public $last_select;
@ -10,11 +10,15 @@ class HarnessDriver implements Drivers\DSODriverInterface
public $last_update;
public $last_delete;
public function __construct(string $dsn, string $username=null, string $password=null, array $options=null)
public function __construct(string $dsn=null, string $username=null, string $password=null, array $options=null)
{
}
public function createTable(string $table, array $virtualColumns) : bool
public function pdo(\PDO $pdo=null) : ?\PDO {
return null;
}
public function prepareEnvironment(string $table, array $schema) : bool
{
//TODO: add tests for this too
return false;

View file

@ -1,23 +0,0 @@
<?php
/* Destructr | https://gitlab.com/byjoby/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');
}
}

View file

@ -1,22 +0,0 @@
<?php
/* Destructr | https://gitlab.com/byjoby/destructr | MIT License */
declare(strict_types=1);
namespace Destructr\LegacyDrivers\SQLite;
use PHPUnit\Framework\TestCase;
use Destructr\Drivers\AbstractDriverTest;
use Destructr\LegacyDrivers\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_OPTIONS = null;
public static function setUpBeforeClass()
{
@unlink(__DIR__.'/driver.test.sqlite');
}
}