From c6f7f1f7831afa4dab02803e18992c0d79fe2c28 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Sat, 12 Feb 2022 00:27:57 +0000 Subject: [PATCH 01/24] .. --- .gitattributes | 8 ++ .../Statement/SelectStatement.php | 133 ++++++++++++++++++ src/QueryBuilder/Statement/Statement.php | 42 ++++++ .../Statement/StatementCollection.php | 84 +++++++++++ 4 files changed, 267 insertions(+) create mode 100644 src/QueryBuilder/Statement/SelectStatement.php create mode 100644 src/QueryBuilder/Statement/Statement.php create mode 100644 src/QueryBuilder/Statement/StatementCollection.php diff --git a/.gitattributes b/.gitattributes index e69de29..3ca1892 100644 --- a/.gitattributes +++ b/.gitattributes @@ -0,0 +1,8 @@ +/tests export-ignore +/phpunit.xml.dist export-ignore +/.scrutinizer.yml export-ignore +/.php-cs-fixer.php export-ignore +/codecov.yml export-ignore +/phpstan.neon.dist export-ignore +/static-loader-creator.php export-ignore +/src/loader.php export-ignore \ No newline at end of file diff --git a/src/QueryBuilder/Statement/SelectStatement.php b/src/QueryBuilder/Statement/SelectStatement.php new file mode 100644 index 0000000..d7ddf73 --- /dev/null +++ b/src/QueryBuilder/Statement/SelectStatement.php @@ -0,0 +1,133 @@ + + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @package Gin0115\Pixie + * @subpackage QueryBuilder\Statement + */ + +namespace Pixie\QueryBuilder\Statement; + +use TypeError; +use Pixie\QueryBuilder\Raw; +use Pixie\JSON\JsonSelector; +use Pixie\QueryBuilder\Statement\Statement; + +class SelectStatement implements Statement +{ + /** + * The field which is being selected + * + * @var string|Raw|JsonSelector + */ + protected $field; + + /** + * The alias for the selected field + * + * @var string|null + */ + protected $alias = null; + + public function __construct($field, ?string $alias = null) + { + // Verify valid field type. + $this->verifyField($field); + $this->field = $field; + $this->alias = $alias; + } + + /** @inheritDoc */ + public function getType(): string + { + return Statement::SELECT; + } + + /** + * Verifies if the passed filed is of a valid type. + * + * @param mixed $field + * @return void + */ + protected function verifyField($field): void + { + if ( + !is_string($field) + && ! is_a($field, Raw::class) + && !is_a($field, JsonSelector::class) + ) { + throw new TypeError("Only string, Raw and JsonSelectors may be used as select fields"); + } + } + + /** + * Checks if the passed field needs to be interpolated. + * + * @return bool TRUE if Raw or JsonSelector, FALSE if string. + */ + public function fieldRequiresInterpolation(): bool + { + return is_a($this->field, Raw::class) || is_a($this->field, JsonSelector::class); + } + + /** + * Allows the passing in of a closure to interpolate the statement. + * + * @psalm-immutable + * @param \Closure(Raw|JsonSelector $field): string $callback + * @return SelectStatement + */ + public function interpolateField(\Closure $callback): SelectStatement + { + $field = $callback($this->field); + return new self($field, $this->alias); + } + + /** + * Gets the field. + * + * @return string|Raw|JsonSelector + */ + public function getField() + { + return $this->field; + } + + /** + * Checks if we have a valid (is string, not empty) alias. + * + * @return bool + */ + public function hasAlias(): bool + { + return is_string($this->alias) && 0 !== \mb_strlen($this->alias); + } + + /** + * Gets the alias + * + * @return string|null + */ + public function getAlias(): ?string + { + return $this->hasAlias() ? $this->alias : null; + } +} diff --git a/src/QueryBuilder/Statement/Statement.php b/src/QueryBuilder/Statement/Statement.php new file mode 100644 index 0000000..2fc5243 --- /dev/null +++ b/src/QueryBuilder/Statement/Statement.php @@ -0,0 +1,42 @@ + + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @package Gin0115\Pixie + * @subpackage QueryBuilder\Statement + */ + +namespace Pixie\QueryBuilder\Statement; + +interface Statement +{ + /** + * Statement Types. + */ + public const SELECT = 'select'; + + /** + * Get the statement type + * + * @return string + */ + public function getType(): string; +} diff --git a/src/QueryBuilder/Statement/StatementCollection.php b/src/QueryBuilder/Statement/StatementCollection.php new file mode 100644 index 0000000..4509404 --- /dev/null +++ b/src/QueryBuilder/Statement/StatementCollection.php @@ -0,0 +1,84 @@ + + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @package Gin0115\Pixie + * @subpackage QueryBuilder\Statement + */ + +namespace Pixie\QueryBuilder\Statement; + +class StatementCollection +{ + + /** + * Holds all the statements + * @var array + */ + protected $statements = []; + + /** + * Adds a statement to the collection + * + * @param string $type + * @param Statement $statement + * @return self + */ + protected function add(string $type, Statement $statement): self + { + $this->statements[$type][] = $statement; + return $this; + } + + /** + * Get all Statements of a certain type. + * + * @param mixed $name + * @return Statement[] + */ + protected function get(string $type): array + { + return \array_key_exists($type, $this->statements) + ? $this->statements[$type] + : []; + } + + /** + * Adds a select statement to the collection. + * + * @param SelectStatement $statement + * @return self + */ + public function addSelect(SelectStatement $statement): self + { + return $this->add(Statement::SELECT, $statement); + } + + /** + * Get all SelectStatements + * + * @return SelectStatement[] + */ + public function getSelect(): array + { + return $this->get(Statement::SELECT); + } +} From 3e5da367332ca58970586cd885ec3bcb0acad300 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Sat, 12 Feb 2022 01:02:25 +0000 Subject: [PATCH 02/24] basis of select statement added with tests --- .../Statement/SelectStatement.php | 6 + tests/Unit/Statement/TestSelectStatement.php | 112 ++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 tests/Unit/Statement/TestSelectStatement.php diff --git a/src/QueryBuilder/Statement/SelectStatement.php b/src/QueryBuilder/Statement/SelectStatement.php index d7ddf73..bff69a0 100644 --- a/src/QueryBuilder/Statement/SelectStatement.php +++ b/src/QueryBuilder/Statement/SelectStatement.php @@ -47,6 +47,12 @@ class SelectStatement implements Statement */ protected $alias = null; + /** + * Creates a Select Statement + * + * @param string|Raw|JsonSelector $field + * @param string|null $alias + */ public function __construct($field, ?string $alias = null) { // Verify valid field type. diff --git a/tests/Unit/Statement/TestSelectStatement.php b/tests/Unit/Statement/TestSelectStatement.php new file mode 100644 index 0000000..f624f78 --- /dev/null +++ b/tests/Unit/Statement/TestSelectStatement.php @@ -0,0 +1,112 @@ + + */ + +namespace Pixie\Tests\Unit\Statement; + +use stdClass; +use TypeError; +use WP_UnitTestCase; +use Pixie\QueryBuilder\Raw; +use Pixie\JSON\JsonSelector; +use Pixie\QueryBuilder\Statement\Statement; +use Pixie\QueryBuilder\Statement\SelectStatement; + +/** + * @group v0.2 + * @group unit + * @group statement + */ +class TestSelectStatement extends WP_UnitTestCase +{ + /** Supplies all types which will throw as fields for statement */ + public function invalidTypeProvider(): array + { + return [ + [1], + [2.5], + [['array']], + [new stdClass()], + [null], + [false] + ]; + } + + /** + * @testdox An exception should be thrown if a none String, Raw or JsonSelector passed as the field of a select statement. + * @dataProvider invalidTypeProvider + */ + public function testThrowsIfNoneStringRawJsonSelectorPassed($field) + { + $this->expectExceptionMessage('Only string, Raw and JsonSelectors may be used as select fields'); + $this->expectException(TypeError::class); + + new SelectStatement($field); + } + + /** @testdox It should be possible to get the correct type from any Statement [SELECT] */ + public function testGetType(): void + { + $this->assertEquals('select', (new SelectStatement('*'))->getType()); + $this->assertEquals(Statement::SELECT, (new SelectStatement('*'))->getType()); + } + + /** @testdox Raw and JsonSelector fields will need to be interpolated before they can be parsed, it should be possible to check if this needs to happen. */ + public function testCanInterpolateField(): void + { + $statement = function ($field): SelectStatement { + return new SelectStatement($field); + }; + + $this->assertFalse($statement('string')->fieldRequiresInterpolation()); + $this->assertTrue($statement(new Raw('string'))->fieldRequiresInterpolation()); + $this->assertTrue($statement(new JsonSelector('string', ['a','b']))->fieldRequiresInterpolation()); + } + + /** @testdox It should be possible to interpolate a field and be given a new instance of a Statement with the resolved field. */ + public function testCanInterpolateFieldWithClosure(): void + { + $statement = new SelectStatement(new Raw('foo-%s', ['boo']), 'alias'); + $newStatement = $statement->interpolateField( + function (Raw $e) { + return sprintf($e->getValue(), ...$e->getBindings()); + } + ); + + // Alias should remain unchanged + $this->assertEquals('alias', $statement->getAlias()); + $this->assertEquals('alias', $newStatement->getAlias()); + + // Field should be whatever is returned from closure + $this->assertEquals('foo-boo', $newStatement->getField()); + + // Should be a new object. + $this->assertNotSame($statement, $newStatement); + } + + /** @testdox It should be possible to check if the statement has an alias and gets it value if it does. */ + public function testGetAliasIfExists(): void + { + $with = new SelectStatement('field', 'with'); + $without = new SelectStatement('field'); + $empty = new SelectStatement('field', ''); + $null = new SelectStatement('field', null); + + $this->assertTrue($with->hasAlias()); + $this->assertFalse($without->hasAlias()); + $this->assertFalse($empty->hasAlias()); + $this->assertFalse($null->hasAlias()); + + $this->assertEquals('with', $with->getAlias()); + $this->assertNull($without->getAlias()); + $this->assertNull($empty->getAlias()); + $this->assertNull($null->getAlias()); + } +} From 7f8e8684a0c93ddb44f4de552dd6728c4e727a39 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Sat, 12 Feb 2022 01:14:09 +0000 Subject: [PATCH 03/24] House Keeping --- README.md | 6 +-- .../Statement/SelectStatement.php | 2 +- .../Statement/StatementCollection.php | 37 ++++--------------- 3 files changed, 11 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 38f7c76..e3c0464 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,9 @@ $thing = QB::table('someTable')->where('something','=', 'something else')->first ## Perquisites -* WordPress 5.7+ (tested upto 5.9) -* PHP 7.1+ (includes support for PHP8) -* MySql 5.7+ or MariaDB 10.2+ +* WordPress 5.6+ `(tested on 5.6, 5.7, 5.8, 5.9)` +* PHP 7.1+ `(tested on 7.1, 7.2, 7.3, 7.4, 8.0, 8.1)` +* MySql 5.7+ `(tested on 5.7)` or MariaDB 10.2+ `(tested on 10.2, 10.3, 10.4, 10.5, 10.6, 10.7)` * Composer (optional) ## Using Composer diff --git a/src/QueryBuilder/Statement/SelectStatement.php b/src/QueryBuilder/Statement/SelectStatement.php index bff69a0..043d566 100644 --- a/src/QueryBuilder/Statement/SelectStatement.php +++ b/src/QueryBuilder/Statement/SelectStatement.php @@ -98,7 +98,7 @@ public function fieldRequiresInterpolation(): bool * Allows the passing in of a closure to interpolate the statement. * * @psalm-immutable - * @param \Closure(Raw|JsonSelector $field): string $callback + * @param \Closure(string|Raw|JsonSelector $field): string $callback * @return SelectStatement */ public function interpolateField(\Closure $callback): SelectStatement diff --git a/src/QueryBuilder/Statement/StatementCollection.php b/src/QueryBuilder/Statement/StatementCollection.php index 4509404..e43526a 100644 --- a/src/QueryBuilder/Statement/StatementCollection.php +++ b/src/QueryBuilder/Statement/StatementCollection.php @@ -31,35 +31,11 @@ class StatementCollection /** * Holds all the statements - * @var array + * @var array{select:SelectStatement[]} */ - protected $statements = []; - - /** - * Adds a statement to the collection - * - * @param string $type - * @param Statement $statement - * @return self - */ - protected function add(string $type, Statement $statement): self - { - $this->statements[$type][] = $statement; - return $this; - } - - /** - * Get all Statements of a certain type. - * - * @param mixed $name - * @return Statement[] - */ - protected function get(string $type): array - { - return \array_key_exists($type, $this->statements) - ? $this->statements[$type] - : []; - } + protected $statements = [ + Statement::SELECT => [] + ]; /** * Adds a select statement to the collection. @@ -69,7 +45,8 @@ protected function get(string $type): array */ public function addSelect(SelectStatement $statement): self { - return $this->add(Statement::SELECT, $statement); + $this->statements[Statement::SELECT][] = $statement; + return $this; } /** @@ -79,6 +56,6 @@ public function addSelect(SelectStatement $statement): self */ public function getSelect(): array { - return $this->get(Statement::SELECT); + return $this->statements[Statement::SELECT]; } } From 5ae5e085d0a447549c417521b59237edbc1b0c36 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Sat, 12 Feb 2022 01:17:48 +0000 Subject: [PATCH 04/24] House Keeping --- src/QueryBuilder/Statement/Statement.php | 1 + .../Statement/StatementCollection.php | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/QueryBuilder/Statement/Statement.php b/src/QueryBuilder/Statement/Statement.php index 2fc5243..d93c15d 100644 --- a/src/QueryBuilder/Statement/Statement.php +++ b/src/QueryBuilder/Statement/Statement.php @@ -32,6 +32,7 @@ interface Statement * Statement Types. */ public const SELECT = 'select'; + public const TABLE = 'table'; /** * Get the statement type diff --git a/src/QueryBuilder/Statement/StatementCollection.php b/src/QueryBuilder/Statement/StatementCollection.php index e43526a..0a7adfe 100644 --- a/src/QueryBuilder/Statement/StatementCollection.php +++ b/src/QueryBuilder/Statement/StatementCollection.php @@ -17,7 +17,7 @@ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * - * @since 0.0.2 + * @since 0.2.0 * @author Glynn Quelch * @license http://www.opensource.org/licenses/mit-license.html MIT License * @package Gin0115\Pixie @@ -28,13 +28,13 @@ class StatementCollection { - /** * Holds all the statements * @var array{select:SelectStatement[]} */ protected $statements = [ - Statement::SELECT => [] + Statement::SELECT => [], + Statement::TABLE => [], ]; /** @@ -58,4 +58,14 @@ public function getSelect(): array { return $this->statements[Statement::SELECT]; } + + /** + * Select statements exist. + * + * @return bool + */ + public function hasSelect(): bool + { + return 0 < count($this->getSelect()); + } } From 7bbdebe02a4a4574873e6c9445cb7142a057c588 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Sat, 12 Feb 2022 01:48:21 +0000 Subject: [PATCH 05/24] created basic table statement with unit tests --- .../Statement/SelectStatement.php | 4 +- .../Statement/Statement.php | 2 +- .../Statement/StatementCollection.php | 2 +- src/Statement/TableStatement.php | 137 ++++++++++++++++++ tests/Unit/Statement/TestSelectStatement.php | 4 +- tests/Unit/Statement/TestTableStatement.php | 111 ++++++++++++++ 6 files changed, 254 insertions(+), 6 deletions(-) rename src/{QueryBuilder => }/Statement/SelectStatement.php (97%) rename src/{QueryBuilder => }/Statement/Statement.php (97%) rename src/{QueryBuilder => }/Statement/StatementCollection.php (98%) create mode 100644 src/Statement/TableStatement.php create mode 100644 tests/Unit/Statement/TestTableStatement.php diff --git a/src/QueryBuilder/Statement/SelectStatement.php b/src/Statement/SelectStatement.php similarity index 97% rename from src/QueryBuilder/Statement/SelectStatement.php rename to src/Statement/SelectStatement.php index 043d566..fa1e2ae 100644 --- a/src/QueryBuilder/Statement/SelectStatement.php +++ b/src/Statement/SelectStatement.php @@ -24,12 +24,12 @@ * @subpackage QueryBuilder\Statement */ -namespace Pixie\QueryBuilder\Statement; +namespace Pixie\Statement; use TypeError; use Pixie\QueryBuilder\Raw; use Pixie\JSON\JsonSelector; -use Pixie\QueryBuilder\Statement\Statement; +use Pixie\Statement\Statement; class SelectStatement implements Statement { diff --git a/src/QueryBuilder/Statement/Statement.php b/src/Statement/Statement.php similarity index 97% rename from src/QueryBuilder/Statement/Statement.php rename to src/Statement/Statement.php index d93c15d..0adb2e4 100644 --- a/src/QueryBuilder/Statement/Statement.php +++ b/src/Statement/Statement.php @@ -24,7 +24,7 @@ * @subpackage QueryBuilder\Statement */ -namespace Pixie\QueryBuilder\Statement; +namespace Pixie\Statement; interface Statement { diff --git a/src/QueryBuilder/Statement/StatementCollection.php b/src/Statement/StatementCollection.php similarity index 98% rename from src/QueryBuilder/Statement/StatementCollection.php rename to src/Statement/StatementCollection.php index 0a7adfe..cdea701 100644 --- a/src/QueryBuilder/Statement/StatementCollection.php +++ b/src/Statement/StatementCollection.php @@ -24,7 +24,7 @@ * @subpackage QueryBuilder\Statement */ -namespace Pixie\QueryBuilder\Statement; +namespace Pixie\Statement; class StatementCollection { diff --git a/src/Statement/TableStatement.php b/src/Statement/TableStatement.php new file mode 100644 index 0000000..8f85839 --- /dev/null +++ b/src/Statement/TableStatement.php @@ -0,0 +1,137 @@ + + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @package Gin0115\Pixie + * @subpackage QueryBuilder\Statement + */ + +namespace Pixie\Statement; + +use TypeError; +use Pixie\QueryBuilder\Raw; +use Pixie\Statement\Statement; + +class TableStatement implements Statement +{ + /** + * The table which is being selected + * + * @var string|Raw + */ + protected $table; + + /** + * The alias for the selected table + * + * @var string|null + */ + protected $alias = null; + + /** + * Creates a Select Statement + * + * @param string|Raw $table + * @param string|null $alias + */ + public function __construct($table, ?string $alias = null) + { + // Verify valid table type. + $this->verifyTable($table); + $this->table = $table; + $this->alias = $alias; + } + + /** @inheritDoc */ + public function getType(): string + { + return Statement::TABLE; + } + + /** + * Verifies if the passed filed is of a valid type. + * + * @param mixed $table + * @return void + */ + protected function verifyTable($table): void + { + if ( + !is_string($table) + && ! is_a($table, Raw::class) + ) { + throw new TypeError("Only string and Raw may be used as tables"); + } + } + + /** + * Checks if the passed table needs to be interpolated. + * + * @return bool TRUE if Raw, FALSE if string. + */ + public function tableRequiresInterpolation(): bool + { + return is_a($this->table, Raw::class); + } + + /** + * Allows the passing in of a closure to interpolate the statement. + * + * @psalm-immutable + * @param \Closure(string|Raw $table): string $callback + * @return TableStatement + */ + public function interpolateField(\Closure $callback): TableStatement + { + $table = $callback($this->table); + return new self($table, $this->alias); + } + + /** + * Gets the table. + * + * @return string|Raw + */ + public function getTable() + { + return $this->table; + } + + /** + * Checks if we have a valid (is string, not empty) alias. + * + * @return bool + */ + public function hasAlias(): bool + { + return is_string($this->alias) && 0 !== \mb_strlen($this->alias); + } + + /** + * Gets the alias + * + * @return string|null + */ + public function getAlias(): ?string + { + return $this->hasAlias() ? $this->alias : null; + } +} diff --git a/tests/Unit/Statement/TestSelectStatement.php b/tests/Unit/Statement/TestSelectStatement.php index f624f78..4ab1fe7 100644 --- a/tests/Unit/Statement/TestSelectStatement.php +++ b/tests/Unit/Statement/TestSelectStatement.php @@ -16,8 +16,8 @@ use WP_UnitTestCase; use Pixie\QueryBuilder\Raw; use Pixie\JSON\JsonSelector; -use Pixie\QueryBuilder\Statement\Statement; -use Pixie\QueryBuilder\Statement\SelectStatement; +use Pixie\Statement\Statement; +use Pixie\Statement\SelectStatement; /** * @group v0.2 diff --git a/tests/Unit/Statement/TestTableStatement.php b/tests/Unit/Statement/TestTableStatement.php new file mode 100644 index 0000000..a4b6904 --- /dev/null +++ b/tests/Unit/Statement/TestTableStatement.php @@ -0,0 +1,111 @@ + + */ + +namespace Pixie\Tests\Unit\Statement; + +use stdClass; +use TypeError; +use WP_UnitTestCase; +use Pixie\QueryBuilder\Raw; +use Pixie\JSON\JsonSelector; +use Pixie\Statement\Statement; +use Pixie\Statement\TableStatement; + +/** + * @group v0.2 + * @group unit + * @group statement + */ +class TestTableStatement extends WP_UnitTestCase +{ + /** Supplies all types which will throw as tables for statement */ + public function invalidTypeProvider(): array + { + return [ + [1], + [2.5], + [['array']], + [new stdClass()], + [null], + [false] + ]; + } + + /** + * @testdox An exception should be thrown if a none String, Raw passed as the table in a statement. + * @dataProvider invalidTypeProvider + */ + public function testThrowsIfNoneStringRawJsonSelectorPassed($table) + { + $this->expectExceptionMessage('Only string and Raw may be used as tables'); + $this->expectException(TypeError::class); + + new TableStatement($table); + } + + /** @testdox It should be possible to get the correct type from any Statement [TABLE] */ + public function testGetType(): void + { + $this->assertEquals('table', (new TableStatement('*'))->getType()); + $this->assertEquals(Statement::TABLE, (new TableStatement('*'))->getType()); + } + + /** @testdox Raw tables will need to be interpolated before they can be parsed, it should be possible to check if this needs to happen. */ + public function testCanInterpolateField(): void + { + $statement = function ($table): TableStatement { + return new TableStatement($table); + }; + + $this->assertFalse($statement('tableName')->tableRequiresInterpolation()); + $this->assertTrue($statement(new Raw('tableName'))->tableRequiresInterpolation()); + } + + /** @testdox It should be possible to interpolate a table and be given a new instance of a Statement with the resolved table. */ + public function testCanInterpolateFieldWithClosure(): void + { + $statement = new TableStatement(new Raw('foo-%s', ['boo']), 'alias'); + $newStatement = $statement->interpolateField( + function (Raw $e) { + return sprintf($e->getValue(), ...$e->getBindings()); + } + ); + + // Alias should remain unchanged + $this->assertEquals('alias', $statement->getAlias()); + $this->assertEquals('alias', $newStatement->getAlias()); + + // Field should be whatever is returned from closure + $this->assertEquals('foo-boo', $newStatement->getTable()); + + // Should be a new object. + $this->assertNotSame($statement, $newStatement); + } + + /** @testdox It should be possible to check if the statement has an alias and gets it value if it does. */ + public function testGetAliasIfExists(): void + { + $with = new TableStatement('table', 'with'); + $without = new TableStatement('table'); + $empty = new TableStatement('table', ''); + $null = new TableStatement('table', null); + + $this->assertTrue($with->hasAlias()); + $this->assertFalse($without->hasAlias()); + $this->assertFalse($empty->hasAlias()); + $this->assertFalse($null->hasAlias()); + + $this->assertEquals('with', $with->getAlias()); + $this->assertNull($without->getAlias()); + $this->assertNull($empty->getAlias()); + $this->assertNull($null->getAlias()); + } +} From 09bc48f18a224174809fee5af8a15f85205f28b6 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Sat, 12 Feb 2022 02:06:29 +0000 Subject: [PATCH 06/24] added in collection tests --- src/Statement/StatementCollection.php | 47 ++++++++++- .../Statement/TestStatementCollection.php | 81 +++++++++++++++++++ 2 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 tests/Unit/Statement/TestStatementCollection.php diff --git a/src/Statement/StatementCollection.php b/src/Statement/StatementCollection.php index cdea701..96be253 100644 --- a/src/Statement/StatementCollection.php +++ b/src/Statement/StatementCollection.php @@ -26,17 +26,30 @@ namespace Pixie\Statement; +use Pixie\Statement\TableStatement; + class StatementCollection { /** * Holds all the statements - * @var array{select:SelectStatement[]} + * + * @var array{select:SelectStatement[],table:TableStatement[]} */ protected $statements = [ Statement::SELECT => [], Statement::TABLE => [], ]; + /** + * Get all the statements + * + * @return array{select:SelectStatement[],table:TableStatement[]} + */ + public function getStatements(): array + { + return $this->statements; + } + /** * Adds a select statement to the collection. * @@ -68,4 +81,36 @@ public function hasSelect(): bool { return 0 < count($this->getSelect()); } + + /** + * Adds a select statement to the collection. + * + * @param TableStatement $statement + * @return self + */ + public function addTable(TableStatement $statement): self + { + $this->statements[Statement::TABLE][] = $statement; + return $this; + } + + /** + * Get all Table Statements + * + * @return TableStatement[] + */ + public function getTable(): array + { + return $this->statements[Statement::TABLE]; + } + + /** + * Table statements exist. + * + * @return bool + */ + public function hasTable(): bool + { + return 0 < count($this->getTable()); + } } diff --git a/tests/Unit/Statement/TestStatementCollection.php b/tests/Unit/Statement/TestStatementCollection.php new file mode 100644 index 0000000..c894319 --- /dev/null +++ b/tests/Unit/Statement/TestStatementCollection.php @@ -0,0 +1,81 @@ + + */ + +namespace Pixie\Tests\Unit\Statement; + +use WP_UnitTestCase; +use Pixie\Statement\Statement; +use Pixie\Statement\TableStatement; +use Pixie\Statement\SelectStatement; +use Pixie\Statement\StatementCollection; + +/** + * @group v0.2 + * @group unit + * @group statement + */ +class TestStatementCollection extends WP_UnitTestCase +{ + + /** @testdox It should be possible to get the contents of the collection */ + public function testGetCollectionItems(): void + { + $collection = new StatementCollection(); + $array = $collection->getStatements(); + + // Check all keys exist + $this->assertArrayHasKey(Statement::SELECT, $array); + $this->assertArrayHasKey('select', $array); + $this->assertArrayHasKey(Statement::TABLE, $array); + $this->assertArrayHasKey('table', $array); + + // Add values, + $collection->addSelect($this->createMock(SelectStatement::class)); + $collection->addTable($this->createMock(TableStatement::class)); + $array = $collection->getStatements(); + $this->assertCount(1, $array['select']); + $this->assertCount(1, $array['table']); + } + + /** @testdox It should be possible to add, fetch select statements and check if any set. */ + public function testSelectStatement(): void + { + $collection = new StatementCollection(); + + // Should be empty + $this->assertFalse($collection->hasSelect()); + $this->assertEmpty($collection->getSelect()); + + $statement = $this->createMock(SelectStatement::class); + $collection->addSelect($statement); + + $this->assertTrue($collection->hasSelect()); + $this->assertCount(1, $collection->getSelect()); + $this->assertContains($statement, $collection->getSelect()); + } + + /** @testdox It should be possible to add, fetch table statements and check if any set. */ + public function testTableStatement(): void + { + $collection = new StatementCollection(); + + // Should be empty + $this->assertFalse($collection->hasTable()); + $this->assertEmpty($collection->getTable()); + + $statement = $this->createMock(TableStatement::class); + $collection->addTable($statement); + + $this->assertTrue($collection->hasTable()); + $this->assertCount(1, $collection->getTable()); + $this->assertContains($statement, $collection->getTable()); + } +} From 287c8d9819f7378ac2600ba3a0d146dd81337196 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Sun, 13 Feb 2022 20:54:36 +0000 Subject: [PATCH 07/24] Select correclty being parsed with all prefixing and parseing happening in the parser, not builder or adaptor. --- src/JSON/JsonHandler.php | 2 +- src/JSON/JsonSelectorHandler.php | 25 +--- src/Parser/Normalizer.php | 126 ++++++++++++++++++ src/Parser/StatementParser.php | 84 ++++++++++++ src/Parser/TablePrefixer.php | 136 ++++++++++++++++++++ src/QueryBuilder/QueryBuilderHandler.php | 45 +++++-- src/QueryBuilder/Raw.php | 10 ++ src/QueryBuilder/TablePrefixer.php | 1 + src/QueryBuilder/WPDBAdapter.php | 90 ++++++++++++- tests/JSON/TestJsonSelectorHandler.php | 22 +--- tests/Unit/Parser/TestStatementParser.php | 119 +++++++++++++++++ tests/Unit/Statement/TestTableStatement.php | 1 - 12 files changed, 609 insertions(+), 52 deletions(-) create mode 100644 src/Parser/Normalizer.php create mode 100644 src/Parser/StatementParser.php create mode 100644 src/Parser/TablePrefixer.php create mode 100644 tests/Unit/Parser/TestStatementParser.php diff --git a/src/JSON/JsonHandler.php b/src/JSON/JsonHandler.php index 08ffb3b..bfd7d3f 100644 --- a/src/JSON/JsonHandler.php +++ b/src/JSON/JsonHandler.php @@ -18,7 +18,7 @@ class JsonHandler public function __construct(Connection $connection) { $this->connection = $connection; - $this->jsonSelectorHandler = new JsonSelectorHandler($connection); + $this->jsonSelectorHandler = new JsonSelectorHandler(); $this->jsonExpressionFactory = new JsonExpressionFactory($connection); } diff --git a/src/JSON/JsonSelectorHandler.php b/src/JSON/JsonSelectorHandler.php index baf8491..e6d0bed 100644 --- a/src/JSON/JsonSelectorHandler.php +++ b/src/JSON/JsonSelectorHandler.php @@ -3,32 +3,9 @@ namespace Pixie\JSON; use Pixie\Exception; -use Pixie\Connection; -use Pixie\HasConnection; -use Pixie\QueryBuilder\TablePrefixer; -class JsonSelectorHandler implements HasConnection +class JsonSelectorHandler { - use TablePrefixer; - - /** @var Connection */ - protected $connection; - - public function __construct(Connection $connection) - { - $this->connection = $connection; - } - - /** - * Returns the current connection instance. - * - * @return connection - */ - public function getConnection(): Connection - { - return $this->connection; - } - /** * Checks if the passed expression is for JSON * this->denotes->json diff --git a/src/Parser/Normalizer.php b/src/Parser/Normalizer.php new file mode 100644 index 0000000..653397e --- /dev/null +++ b/src/Parser/Normalizer.php @@ -0,0 +1,126 @@ + + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @package Gin0115\Pixie + * @subpackage Parser + */ + +namespace Pixie\Parser; + +use Pixie\Connection; +use Pixie\HasConnection; +use Pixie\QueryBuilder\Raw; +use Pixie\JSON\JsonSelector; +use Pixie\Parser\TablePrefixer; +use Pixie\JSON\JsonSelectorHandler; +use Pixie\Statement\SelectStatement; +use Pixie\JSON\JsonExpressionFactory; + +class Normalizer +{ + /** + * @var Connection + */ + protected $connection; + + /** + * Handler for JSON selectors + * + * @var JsonSelectorHandler + */ + protected $jsonSelectors; + + /** + * JSON expression factory + * + * @var JsonExpressionFactory + */ + protected $jsonExpressions; + + /** + * Access to the table prefixer. + * + * @var TablePrefixer + */ + protected $tablePrefixer; + + public function __construct(Connection $connection, TablePrefixer $tablePrefixer) + { + $this->connection = $connection; + $this->tablePrefixer = $tablePrefixer; + $this->jsonSelectors = new JsonSelectorHandler(); + $this->jsonExpressions = new JsonExpressionFactory($connection); + + // Create table prefixer. + } + + /** + * Access to the connection. + * + * @return \Pixie\Connection + */ + public function getConnection(): Connection + { + return $this->connection; + } + + /** + * + * + * @param \Pixie\Statement\SelectStatement $statement + * @return string + */ + public function selectStatement(SelectStatement $statement): string + { + $field = $statement->getField(); + switch (true) { + // Is JSON Arrow Selector. + case is_string($field) && $this->jsonSelectors->isJsonSelector($field): + // Cast as JsonSelector + $field = $this->jsonSelectors->toJsonSelector($field); + // Get & Return SQL Expression as RAW + return $this->jsonExpressions->extractAndUnquote( + $this->tablePrefixer->field($field->getColumn()), + $field->getNodes() + )->getValue(); + + // If JSON selector + case is_a($field, JsonSelector::class): + // Get & Return SQL Expression as RAW + return $this->jsonExpressions->extractAndUnquote( + $this->tablePrefixer->field($field->getColumn()), + $field->getNodes() + )->getValue(); + + // RAW + case is_a($field, Raw::class): + // Return the extrapolated Raw expression. + return ! $field->hasBindings() + ? $this->tablePrefixer->field($field->getValue()) + : sprintf($field->getValue(), ...$field->getBindings()); + + // Assume fallback as string. + default: + return $this->tablePrefixer->field($field); + } + } +} diff --git a/src/Parser/StatementParser.php b/src/Parser/StatementParser.php new file mode 100644 index 0000000..069887b --- /dev/null +++ b/src/Parser/StatementParser.php @@ -0,0 +1,84 @@ + + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @package Gin0115\Pixie + * @subpackage Parser + */ + +namespace Pixie\Parser; + +use Pixie\Connection; +use Pixie\Statement\SelectStatement; + +class StatementParser +{ + protected const TEMPLATE_AS = "%s AS %s"; + + /** + * @var Connection + */ + protected $connection; + + /** @var Normalizer */ + protected $normalizer; + + public function __construct(Connection $connection) + { + $this->connection = $connection; + + // Create the table prefixer. + $adapterConfig = $connection->getAdapterConfig(); + $prefix = isset($adapterConfig[Connection::PREFIX]) + ? $adapterConfig[Connection::PREFIX] + : null; + + $this->normalizer = new Normalizer( + $connection, + new TablePrefixer($prefix) + ); + } + + /** + * Normalizes and Parsers an array of SelectStatements. + * + * @param SelectStatement[]|mixed[] $select + * @return string + */ + public function parseSelect(array $select): string + { + // Remove any none SelectStatements + $select = array_filter($select, function ($statement): bool { + return is_a($statement, SelectStatement::class); + }); + + // Cast to string, with or without alias, + $select = array_map(function (SelectStatement $value): string { + $alias = $value->getAlias(); + $value = $this->normalizer->selectStatement($value); + return null === $alias + ? $value + : sprintf(self::TEMPLATE_AS, $value, $alias); + }, $select); + + return join(', ', $select); + } +} diff --git a/src/Parser/TablePrefixer.php b/src/Parser/TablePrefixer.php new file mode 100644 index 0000000..ad5bb07 --- /dev/null +++ b/src/Parser/TablePrefixer.php @@ -0,0 +1,136 @@ + + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @package Gin0115\Pixie + * @subpackage Parser + */ + +namespace Pixie\Parser; + +use Closure; +use Pixie\QueryBuilder\Raw; +use Pixie\JSON\JsonSelector; + +class TablePrefixer +{ + /** + * @var string|null + */ + protected $tablePrefix; + + + public function __construct(?string $tablePrefix) + { + $this->tablePrefix = $tablePrefix; + } + + /** + * Applies the prefix if it applies to a field. + * + * tableName.columnName would be prefixed. + * columnName would not be prefixed. + * + * Raw, JsonSelector and Closure values skipped, these should prefixed when interpolated. + * + * @phpstan-template T of string|string[]|Raw|Raw[]|JsonSelector|JsonSelector[]|Closure|Closure[] + * @phpstan-param T $value + * @phpstan-return T + */ + public function field($value) + { + return $this->addTablePrefix($value, true); + } + + /** + * Applies the prefix to a table name. + * + * Raw, JsonSelector and Closure values skipped, these should prefixed when interpolated. + * + * @phpstan-template T of string|string[]|Raw|Raw[]|JsonSelector|JsonSelector[]|Closure|Closure[] + * @phpstan-param T $tableName + * @phpstan-return T|null + */ + public function table($tableName) + { + return $this->addTablePrefix($tableName, false); + } + + /** + * Add table prefix (if given) on given string. + * + * @phpstan-template T of string|string[]|Raw|Raw[]|JsonSelector|JsonSelector[]|Closure|Closure[] + * @phpstan-param T $values + * @param bool $tableFieldMix If we have mixes of field and table names with a "." + * + * @phpstan-return T + */ + public function addTablePrefix($values, bool $tableFieldMix = true) + { + if (is_null($this->tablePrefix)) { + return $values; + } + + // $value will be an array and we will add prefix to all table names + + // If supplied value is not an array then make it one + $single = false; + if (!is_array($values)) { + $values = [$values]; + // We had single value, so should return a single value + $single = true; + } + + $return = []; + + foreach ($values as $key => $value) { + // It's a raw query, just add it to our return array and continue next + if ($value instanceof Raw || $value instanceof Closure || $value instanceof JsonSelector) { + $return[$key] = $value; + continue; + } + + // If key is not integer, it is likely a alias mapping, + // so we need to change prefix target + $target = &$value; + if (!is_int($key)) { + $target = &$key; + } + + // Do prefix if the target is an expression or function. + if ( + !$tableFieldMix + || ( + is_string($target) // Must be a string + && strpos($target, $this->tablePrefix) !== 0 // Inst already added. + && (bool) preg_match('/^[A-Za-z0-9_.]+$/', $target) // Can only contain letters, numbers, underscore and full stops + && 1 === \substr_count($target, '.') // Contains a single full stop ONLY. + ) + ) { + $target = $this->tablePrefix . $target; + } + + $return[$key] = $value; + } + // If we had single value then we should return a single value (end value of the array) + return true === $single ? array_values($return)[0] : $return; // @phpstan-ignore-line + } +} diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index 4265d3a..8c3ff10 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -8,18 +8,20 @@ use Pixie\Binding; use Pixie\Exception; use Pixie\Connection; + +use function mb_strlen; + use Pixie\HasConnection; use Pixie\JSON\JsonHandler; use Pixie\QueryBuilder\Raw; use Pixie\Hydration\Hydrator; -use Pixie\JSON\JsonSelectorHandler; use Pixie\QueryBuilder\JoinBuilder; use Pixie\QueryBuilder\QueryObject; use Pixie\QueryBuilder\Transaction; use Pixie\QueryBuilder\WPDBAdapter; +use Pixie\Statement\SelectStatement; use Pixie\QueryBuilder\TablePrefixer; - -use function mb_strlen; +use Pixie\Statement\StatementCollection; class QueryBuilderHandler implements HasConnection { @@ -38,6 +40,9 @@ class QueryBuilderHandler implements HasConnection */ protected $statements = []; + /** @var StatementCollection */ + protected $statementCollection; + /** * @var wpdb */ @@ -111,6 +116,9 @@ final public function __construct( // Setup JSON Selector handler. $this->jsonHandler = new JsonHandler($connection); + + // Setup statement collection. + $this->statementCollection = new StatementCollection(); } /** @@ -519,6 +527,10 @@ public function getQuery(string $type = 'select', $dataToBePassed = []) throw new Exception($type . ' is not a known type.', 2); } + if ('select' === $type) { + $queryArr = $this->adapterInstance->selectCol($this->statementCollection, [], $this->statements); + } + $queryArr = $this->adapterInstance->$type($this->statements, $dataToBePassed); return new QueryObject($queryArr['sql'], $queryArr['bindings'], $this->dbInstance); @@ -702,7 +714,7 @@ public function delete() public function table(...$tables) { $instance = $this->constructCurrentBuilderClass($this->connection); - $this->setFetchMode($this->getFetchMode(), $this->hydratorConstructorArgs); + $instance->setFetchMode($this->getFetchMode(), $this->hydratorConstructorArgs); $tables = $this->addTablePrefix($tables, false); $instance->addStatement('tables', $tables); @@ -723,6 +735,8 @@ public function from(...$tables): self } /** + * Select which fields should be returned in the results. + * * @param string|string[]|Raw[]|array $fields * * @return static @@ -734,23 +748,34 @@ public function select($fields): self } foreach ($fields as $field => $alias) { - // If we have a JSON expression - if ($this->jsonHandler->isJsonSelector($field)) { - $field = $this->jsonHandler->extractAndUnquoteFromJsonSelector($field); - } - // If no alias passed, but field is for JSON. thrown an exception. if (is_numeric($field) && is_string($alias) && $this->jsonHandler->isJsonSelector($alias)) { throw new Exception("An alias must be used if you wish to select from JSON Object", 1); } + /** V0.2 */ + $statement = is_numeric($field) + ? new SelectStatement($alias) + : new SelectStatement($field, $alias); + $this->statementCollection->addSelect($statement); + + /** REMOVE BELOW IN V0.2 */ + // If we have a JSON expression + if ($this->jsonHandler->isJsonSelector($field)) { + + /** @var string $field */ + $field = $this->jsonHandler->extractAndUnquoteFromJsonSelector($field); // @phpstan-ignore-line + } + $field = $this->addTablePrefix($field); + // Treat each array as a single table, to retain order added $field = is_numeric($field) ? $field = $alias // If single colum : $field = [$field => $alias]; // Has alias - $field = $this->addTablePrefix($field); + $this->addStatement('selects', $field); + /** REMOVE ABOVE IN V0.2 */ } return $this; diff --git a/src/QueryBuilder/Raw.php b/src/QueryBuilder/Raw.php index c757ede..ce47bbb 100644 --- a/src/QueryBuilder/Raw.php +++ b/src/QueryBuilder/Raw.php @@ -45,6 +45,16 @@ public function getBindings(): array return $this->bindings; } + /** + * Checks if any bindings defined. + * + * @return bool + */ + public function hasBindings(): bool + { + return 0 !== count($this->bindings); + } + /** * Returns the current value held. * diff --git a/src/QueryBuilder/TablePrefixer.php b/src/QueryBuilder/TablePrefixer.php index 6cf3d6e..e8eb6c8 100644 --- a/src/QueryBuilder/TablePrefixer.php +++ b/src/QueryBuilder/TablePrefixer.php @@ -54,6 +54,7 @@ public function addTablePrefix($values, bool $tableFieldMix = true) !$tableFieldMix || ( is_string($target) // Must be a string + && strpos($target, $this->getTablePrefix()) !== 0 // Inst already added. && (bool) preg_match('/^[A-Za-z0-9_.]+$/', $target) // Can only contain letters, numbers, underscore and full stops && 1 === \substr_count($target, '.') // Contains a single full stop ONLY. ) diff --git a/src/QueryBuilder/WPDBAdapter.php b/src/QueryBuilder/WPDBAdapter.php index e39393d..24ee768 100644 --- a/src/QueryBuilder/WPDBAdapter.php +++ b/src/QueryBuilder/WPDBAdapter.php @@ -5,10 +5,16 @@ use Closure; use Pixie\Binding; use Pixie\Exception; + use Pixie\Connection; + use Pixie\QueryBuilder\Raw; -use Pixie\QueryBuilder\NestedCriteria; +use Pixie\Parser\StatementParser; + +use Pixie\Statement\SelectStatement; +use Pixie\QueryBuilder\NestedCriteria; +use Pixie\Statement\StatementCollection; use function is_bool; use function is_float; @@ -30,6 +36,88 @@ public function __construct(Connection $connection) $this->connection = $connection; } + /** This is a mock for the new parser based select method. */ + public function selectCol(StatementCollection $col, $data, $statements) // @phpstan-ignore-line + { + if (!array_key_exists('tables', $statements)) { + throw new Exception('No table specified.', 3); + } elseif (!array_key_exists('selects', $statements)) { + $statements['selects'][] = '*'; + } + + $parser = new StatementParser($this->connection); + if (!$col->hasSelect()) { + $col->addSelect(new SelectStatement('*')); + } + $_selects = $parser->parseSelect($col->getSelect()); + + + + // From + $tables = $this->arrayStr($statements['tables'], ', '); + // Select + $selects = $this->arrayStr($statements['selects'], ', '); + + // Wheres + list($whereCriteria, $whereBindings) = $this->buildCriteriaWithType($statements, 'wheres', 'WHERE'); + + // Group bys + $groupBys = ''; + if (isset($statements['groupBys']) && $groupBys = $this->arrayStr($statements['groupBys'], ', ')) { + $groupBys = 'GROUP BY ' . $groupBys; + } + + // Order bys + $orderBys = ''; + if (isset($statements['orderBys']) && is_array($statements['orderBys'])) { + foreach ($statements['orderBys'] as $orderBy) { + $field = $this->wrapSanitizer($orderBy['field']); + if ($field instanceof Closure) { + continue; + } + $orderBys .= $field . ' ' . $orderBy['type'] . ', '; + } + + if ($orderBys = trim($orderBys, ', ')) { + $orderBys = 'ORDER BY ' . $orderBys; + } + } + + // Limit and offset + $limit = isset($statements['limit']) ? 'LIMIT ' . (int) $statements['limit'] : ''; + $offset = isset($statements['offset']) ? 'OFFSET ' . (int) $statements['offset'] : ''; + + // Having + list($havingCriteria, $havingBindings) = $this->buildCriteriaWithType($statements, 'havings', 'HAVING'); + + // Joins + $joinString = $this->buildJoin($statements); + + /** @var string[] */ + $sqlArray = [ + 'SELECT' . (isset($statements['distinct']) ? ' DISTINCT' : ''), + $selects, + 'FROM', + $tables, + $joinString, + $whereCriteria, + $groupBys, + $havingCriteria, + $orderBys, + $limit, + $offset, + ]; + + $sql = $this->concatenateQuery($sqlArray); + + $bindings = array_merge( + $whereBindings, + $havingBindings + ); + + return compact('sql', 'bindings'); + } + /** * Build select query string and bindings * diff --git a/tests/JSON/TestJsonSelectorHandler.php b/tests/JSON/TestJsonSelectorHandler.php index d6a67c3..d7f318b 100644 --- a/tests/JSON/TestJsonSelectorHandler.php +++ b/tests/JSON/TestJsonSelectorHandler.php @@ -18,14 +18,6 @@ class TestJsonSelectorHandler extends WP_UnitTestCase { - /** @testdox It should be possible to access the Connection used in the Handler. [ALSO MEETS REQUIREMENTS OF THE TablePrefixer TRAIT] */ - public function testCanGetConnection(): void - { - $connection = $this->createMock(Connection::class); - $handler = new JsonSelectorHandler($connection); - $this->assertSame($connection, $handler->getConnection()); - } - /** @testdox It should be possible to check if a value is a valid JSON selector */ public function testIsJsonSelector(): void { @@ -41,7 +33,7 @@ public function testIsJsonSelector(): void ['three->deep->eep', true], ]; - $handler = new JsonSelectorHandler($this->createMock(Connection::class)); + $handler = new JsonSelectorHandler(); foreach ($cases as list($val,$result)) { $this->assertTrue( @@ -54,7 +46,7 @@ public function testIsJsonSelector(): void /** @testdox It should be possible to create an instance of a JsonSelector model from an expression. */ public function testAsSelector(): void { - $handler = new JsonSelectorHandler($this->createMock(Connection::class)); + $handler = new JsonSelectorHandler(); $selector = $handler->toJsonSelector('column->node1->node2'); $this->assertEquals('column', $selector->getColumn()); $this->assertEquals(['node1','node2'], $selector->getNodes()); @@ -63,7 +55,7 @@ public function testAsSelector(): void /** @testdox It should be possible to get the column from a JSON selector */ public function testGetColumn(): void { - $handler = new JsonSelectorHandler($this->createMock(Connection::class)); + $handler = new JsonSelectorHandler(); $this->assertSame('column', $handler->getColumn('column->node')); } @@ -71,21 +63,21 @@ public function testGetColumn(): void public function testGetColumnException(): void { $this->expectExceptionMessage('JSON expression must contain at least 2 values, the table column and at least 1 node.'); - (new JsonSelectorHandler($this->createMock(Connection::class)))->getColumn('missing'); + (new JsonSelectorHandler())->getColumn('missing'); } /** @testdox It should be possible to get the nodes from a JSON selector */ public function testGetNodes(): void { - $handler = new JsonSelectorHandler($this->createMock(Connection::class)); + $handler = new JsonSelectorHandler(); $this->assertSame(['node'], $handler->getNodes('column->node')); $this->assertSame(['node', 'second'], $handler->getNodes('column->node->second')); } - /** @testdox Attemping to get the nodes of an invalid expression should result in an exception */ + /** @testdox Attempting to get the nodes of an invalid expression should result in an exception */ public function testGetNodesException(): void { $this->expectExceptionMessage('JSON expression must contain at least 2 values, the table column and at least 1 node.'); - (new JsonSelectorHandler($this->createMock(Connection::class)))->getNodes('missing'); + (new JsonSelectorHandler())->getNodes('missing'); } } diff --git a/tests/Unit/Parser/TestStatementParser.php b/tests/Unit/Parser/TestStatementParser.php new file mode 100644 index 0000000..8aa3e99 --- /dev/null +++ b/tests/Unit/Parser/TestStatementParser.php @@ -0,0 +1,119 @@ + + */ + +namespace Pixie\Tests\Unit\Statement; + +use stdClass; +use TypeError; +use WP_UnitTestCase; +use Pixie\Connection; +use Pixie\QueryBuilder\Raw; +use Pixie\JSON\JsonSelector; +use Pixie\Tests\Logable_WPDB; +use Pixie\Statement\Statement; +use Pixie\Parser\StatementParser; +use Pixie\Tests\SQLAssertionsTrait; +use Pixie\Statement\SelectStatement; +use Pixie\Statement\StatementCollection; + +/** + * @group v0.2 + * @group unit + * @group parser + */ +class TestStatementParser extends WP_UnitTestCase +{ + use SQLAssertionsTrait; + + /** Mocked WPDB instance. + * @var Logable_WPDB + */ + private $wpdb; + + public function setUp(): void + { + $this->wpdb = new Logable_WPDB(); + parent::setUp(); + } + + /** + * Create an instance of the StatementParser with a defined connection config. + * + * @param array $connectionConfig + * @return \Pixie\Parser\StatementParser + */ + public function getParser(array $connectionConfig = []): StatementParser + { + return new StatementParser(new Connection($this->wpdb, $connectionConfig)); + } + + /** @testdox Is should be possible to parse all expected select values from string, json arrow and selector objects and raw expressions and have them returned as a valid SQL fragment. */ + public function testSelectParserWithAcceptTypes(): void + { + $collection = new StatementCollection(); + // Expected user inputs + $collection->addSelect(new SelectStatement('simpleCol')); + $collection->addSelect(new SelectStatement('table.simpleCol')); + $collection->addSelect(new SelectStatement('json->arrow->selector')); + $collection->addSelect(new SelectStatement('table.json->arrow->selector')); + // Expected internal inputs. + $collection->addSelect(new SelectStatement(new JsonSelector('jsSimpleCol', ['nodeA', 'nodeB']))); + $collection->addSelect(new SelectStatement(new JsonSelector('table.jsSimpleCol', ['nodeA', 'nodeB']))); + $collection->addSelect(new SelectStatement(Raw::val('rawSimpleCol'))); + $collection->addSelect(new SelectStatement(Raw::val('table.rawSimpleCol'))); + + $parsed = $this->getParser([Connection::PREFIX => 'pfx_']) + ->parseSelect($collection->getSelect()); + + $this->assertStringContainsString('simpleCol', $parsed); + $this->assertStringContainsString('pfx_table.simpleCol', $parsed); + $this->assertStringContainsString('JSON_UNQUOTE(JSON_EXTRACT(json, "$.arrow.selector"))', $parsed); + $this->assertStringContainsString('JSON_UNQUOTE(JSON_EXTRACT(pfx_table.json, "$.arrow.selector"))', $parsed); + $this->assertStringContainsString('JSON_UNQUOTE(JSON_EXTRACT(jsSimpleCol, "$.nodeA.nodeB"))', $parsed); + $this->assertStringContainsString('JSON_UNQUOTE(JSON_EXTRACT(pfx_table.jsSimpleCol, "$.nodeA.nodeB"))', $parsed); + $this->assertStringContainsString('rawSimpleCol', $parsed); + $this->assertStringContainsString('pfx_table.rawSimpleCol', $parsed); + + // Verify as valid SQL. + $this->assertValidSQL(sprintf("SELECT %s FROM fooTable;", $parsed)); + } + + /** @testdox Is should be possible to parse all expected select with aliases values from string, json arrow and selector objects and raw expressions and have them returned as a valid SQL fragment. */ + public function testSelectParserWithAcceptTypesWithAliases(): void + { + $collection = new StatementCollection(); + // Expected user inputs + $collection->addSelect(new SelectStatement('simpleCol', 'alias')); + $collection->addSelect(new SelectStatement('table.simpleCol', 'alias')); + $collection->addSelect(new SelectStatement('json->arrow->selector', 'alias')); + $collection->addSelect(new SelectStatement('table.json->arrow->selector', 'alias')); + // Expected internal inputs. + $collection->addSelect(new SelectStatement(new JsonSelector('jsSimpleCol', ['nodeA', 'nodeB']), 'alias')); + $collection->addSelect(new SelectStatement(new JsonSelector('table.jsSimpleCol', ['nodeA', 'nodeB']), 'alias')); + $collection->addSelect(new SelectStatement(Raw::val('rawSimpleCol'), 'alias')); + $collection->addSelect(new SelectStatement(Raw::val('table.rawSimpleCol'), 'alias')); + + $parsed = $this->getParser([Connection::PREFIX => 'egh_']) + ->parseSelect($collection->getSelect()); + + $this->assertStringContainsString('simpleCol AS alias', $parsed); + $this->assertStringContainsString('egh_table.simpleCol AS alias', $parsed); + $this->assertStringContainsString('JSON_UNQUOTE(JSON_EXTRACT(json, "$.arrow.selector")) AS alias', $parsed); + $this->assertStringContainsString('JSON_UNQUOTE(JSON_EXTRACT(egh_table.json, "$.arrow.selector")) AS alias', $parsed); + $this->assertStringContainsString('JSON_UNQUOTE(JSON_EXTRACT(jsSimpleCol, "$.nodeA.nodeB")) AS alias', $parsed); + $this->assertStringContainsString('JSON_UNQUOTE(JSON_EXTRACT(egh_table.jsSimpleCol, "$.nodeA.nodeB")) AS alias', $parsed); + $this->assertStringContainsString('rawSimpleCol AS alias', $parsed); + $this->assertStringContainsString('egh_table.rawSimpleCol', $parsed); + + // Verify as valid SQL. + $this->assertValidSQL(sprintf("SELECT %s FROM fooTable;", $parsed)); + } +} diff --git a/tests/Unit/Statement/TestTableStatement.php b/tests/Unit/Statement/TestTableStatement.php index a4b6904..37cd8c0 100644 --- a/tests/Unit/Statement/TestTableStatement.php +++ b/tests/Unit/Statement/TestTableStatement.php @@ -15,7 +15,6 @@ use TypeError; use WP_UnitTestCase; use Pixie\QueryBuilder\Raw; -use Pixie\JSON\JsonSelector; use Pixie\Statement\Statement; use Pixie\Statement\TableStatement; From 9bda5af5a00b89ff7a470f4f3afc9ac69baf7f31 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Sun, 13 Feb 2022 21:01:42 +0000 Subject: [PATCH 08/24] House Keeping --- src/Parser/StatementParser.php | 12 +++++++++++- src/QueryBuilder/QueryBuilderHandler.php | 4 ++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Parser/StatementParser.php b/src/Parser/StatementParser.php index 069887b..36559b5 100644 --- a/src/Parser/StatementParser.php +++ b/src/Parser/StatementParser.php @@ -44,14 +44,24 @@ class StatementParser public function __construct(Connection $connection) { $this->connection = $connection; + $this->normalizer = $this->createNormalizer($connection); + } + /** + * Creates a full populated instance of the normalizer + * + * @param Connection $connection + * @return Normalizer + */ + private function createNormalizer($connection): Normalizer + { // Create the table prefixer. $adapterConfig = $connection->getAdapterConfig(); $prefix = isset($adapterConfig[Connection::PREFIX]) ? $adapterConfig[Connection::PREFIX] : null; - $this->normalizer = new Normalizer( + return new Normalizer( $connection, new TablePrefixer($prefix) ); diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index 8c3ff10..408eaff 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -9,9 +9,8 @@ use Pixie\Exception; use Pixie\Connection; -use function mb_strlen; - use Pixie\HasConnection; + use Pixie\JSON\JsonHandler; use Pixie\QueryBuilder\Raw; use Pixie\Hydration\Hydrator; @@ -22,6 +21,7 @@ use Pixie\Statement\SelectStatement; use Pixie\QueryBuilder\TablePrefixer; use Pixie\Statement\StatementCollection; +use function mb_strlen; class QueryBuilderHandler implements HasConnection { From 158207ce89f2f263e4099c0c8925c12e7bbfd8e8 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Tue, 15 Feb 2022 00:04:47 +0000 Subject: [PATCH 09/24] Table statements now with parser and normalizer --- src/Connection.php | 1 + src/Parser/Normalizer.php | 121 +++++++++++++++------- src/Parser/StatementParser.php | 30 +++++- src/QueryBuilder/QueryBuilderHandler.php | 17 +++ src/QueryBuilder/WPDBAdapter.php | 2 +- src/WpdbHandler.php | 70 +++++++++++++ tests/Unit/Parser/TestStatementParser.php | 27 +++-- 7 files changed, 221 insertions(+), 47 deletions(-) create mode 100644 src/WpdbHandler.php diff --git a/src/Connection.php b/src/Connection.php index e4d52e2..8133eb0 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -15,6 +15,7 @@ class Connection public const PREFIX = 'prefix'; public const SHOW_ERRORS = 'show_errors'; public const USE_WPDB_PREFIX = 'use_wpdb_prefix'; + public const THROW_ERRORS = 'throw_errors'; /** * @var string diff --git a/src/Parser/Normalizer.php b/src/Parser/Normalizer.php index 653397e..db6fe37 100644 --- a/src/Parser/Normalizer.php +++ b/src/Parser/Normalizer.php @@ -26,26 +26,28 @@ namespace Pixie\Parser; -use Pixie\Connection; -use Pixie\HasConnection; +use Pixie\WpdbHandler; use Pixie\QueryBuilder\Raw; use Pixie\JSON\JsonSelector; use Pixie\Parser\TablePrefixer; use Pixie\JSON\JsonSelectorHandler; +use Pixie\Statement\TableStatement; use Pixie\Statement\SelectStatement; use Pixie\JSON\JsonExpressionFactory; class Normalizer { /** - * @var Connection + * @var WpdbHandler + * @since 0.2.0 */ - protected $connection; + protected $wpdbHandler; /** * Handler for JSON selectors * * @var JsonSelectorHandler + * @since 0.2.0 */ protected $jsonSelectors; @@ -53,6 +55,7 @@ class Normalizer * JSON expression factory * * @var JsonExpressionFactory + * @since 0.2.0 */ protected $jsonExpressions; @@ -60,34 +63,31 @@ class Normalizer * Access to the table prefixer. * * @var TablePrefixer + * @since 0.2.0 */ protected $tablePrefixer; - public function __construct(Connection $connection, TablePrefixer $tablePrefixer) - { - $this->connection = $connection; + public function __construct( + WpdbHandler $wpdbHandler, + TablePrefixer $tablePrefixer, + JsonSelectorHandler $jsonSelectors, + JsonExpressionFactory $jsonExpressions + ) { + $this->wpdbHandler = $wpdbHandler; $this->tablePrefixer = $tablePrefixer; - $this->jsonSelectors = new JsonSelectorHandler(); - $this->jsonExpressions = new JsonExpressionFactory($connection); - - // Create table prefixer. - } - - /** - * Access to the connection. - * - * @return \Pixie\Connection - */ - public function getConnection(): Connection - { - return $this->connection; + $this->jsonSelectors = $jsonSelectors; + $this->jsonExpressions = $jsonExpressions; } /** + * Normalize all select statements into strings * + * Accepts string or (string) JSON Arrow Selectors, + * JsonSelector Objects and Raw Objects as fields. * * @param \Pixie\Statement\SelectStatement $statement * @return string + * @since 0.2.0 */ public function selectStatement(SelectStatement $statement): string { @@ -95,32 +95,79 @@ public function selectStatement(SelectStatement $statement): string switch (true) { // Is JSON Arrow Selector. case is_string($field) && $this->jsonSelectors->isJsonSelector($field): - // Cast as JsonSelector - $field = $this->jsonSelectors->toJsonSelector($field); - // Get & Return SQL Expression as RAW - return $this->jsonExpressions->extractAndUnquote( - $this->tablePrefixer->field($field->getColumn()), - $field->getNodes() - )->getValue(); + return $this->normalizeJsonArrowSelector($field); // If JSON selector case is_a($field, JsonSelector::class): - // Get & Return SQL Expression as RAW - return $this->jsonExpressions->extractAndUnquote( - $this->tablePrefixer->field($field->getColumn()), - $field->getNodes() - )->getValue(); + return $this->normalizeJsonSelector($field); // RAW case is_a($field, Raw::class): - // Return the extrapolated Raw expression. - return ! $field->hasBindings() - ? $this->tablePrefixer->field($field->getValue()) - : sprintf($field->getValue(), ...$field->getBindings()); + return $this->normalizeRaw($field); // Assume fallback as string. default: return $this->tablePrefixer->field($field); } } + + /** + * Normalize all table states into strings + * + * Accepts either string or RAW expression. + * + * @param \Pixie\Statement\TableStatement $statement + * @return string + */ + public function tableStatement(TableStatement $statement): string + { + $table = $statement->getTable(); + return is_a($table, Raw::class) + ? $this->normalizeRaw($table) + : $this->tablePrefixer->table($table) ?? $table; + } + + /** + * Interpolates a raw expression + * + * @param \Pixie\QueryBuilder\Raw $raw + * @return string + */ + private function normalizeRaw(Raw $raw): string + { + return $this->wpdbHandler->interpolateQuery( + $raw->getValue(), + $raw->getBindings() + ); + } + + /** + * Extract from JSON Arrow selector to string representation. + * + * @param string $selector + * @param bool $isField If set to true with apply table prefix as a field (false for table) + * @return string + */ + private function normalizeJsonArrowSelector(string $selector, bool $isField = true): string + { + $selector = $this->jsonSelectors->toJsonSelector($selector); + return $this->normalizeJsonSelector($selector); + } + + /** + * Extract from JSON Selector to string representation + * + * @param \Pixie\JSON\JsonSelector $selector + * @param bool $isField If set to true with apply table prefix as a field (false for table) + * @return string + */ + private function normalizeJsonSelector(JsonSelector $selector, bool $isField = true): string + { + $column = $isField === \true + ? $this->tablePrefixer->field($selector->getColumn()) + : $this->tablePrefixer->table($selector->getColumn()) ?? $selector->getColumn(); + + return $this->jsonExpressions->extractAndUnquote($column, $selector->getNodes()) + ->getValue(); + } } diff --git a/src/Parser/StatementParser.php b/src/Parser/StatementParser.php index 36559b5..3dee23e 100644 --- a/src/Parser/StatementParser.php +++ b/src/Parser/StatementParser.php @@ -27,7 +27,11 @@ namespace Pixie\Parser; use Pixie\Connection; +use Pixie\WpdbHandler; +use Pixie\JSON\JsonSelectorHandler; +use Pixie\Statement\TableStatement; use Pixie\Statement\SelectStatement; +use Pixie\JSON\JsonExpressionFactory; class StatementParser { @@ -62,8 +66,10 @@ private function createNormalizer($connection): Normalizer : null; return new Normalizer( - $connection, - new TablePrefixer($prefix) + new WpdbHandler($connection), + new TablePrefixer($prefix), + new JsonSelectorHandler(), + new JsonExpressionFactory($connection) ); } @@ -91,4 +97,24 @@ public function parseSelect(array $select): string return join(', ', $select); } + + /** + * Normalizes and Parsers an array of TableStatements + * + * @param TableStatement[] $tables + * @return string + */ + public function parseTable(array $tables): string + { + // Remove any none TableStatement + $tables = array_filter($tables, function ($statement): bool { + return is_a($statement, TableStatement::class); + }); + + $tables = array_map(function (TableStatement $table): string { + return $this->normalizer->tableStatement($table); + }, $tables); + + return join(', ', $tables); + } } diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index 408eaff..7107e43 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -18,6 +18,7 @@ use Pixie\QueryBuilder\QueryObject; use Pixie\QueryBuilder\Transaction; use Pixie\QueryBuilder\WPDBAdapter; +use Pixie\Statement\TableStatement; use Pixie\Statement\SelectStatement; use Pixie\QueryBuilder\TablePrefixer; use Pixie\Statement\StatementCollection; @@ -715,8 +716,15 @@ public function table(...$tables) { $instance = $this->constructCurrentBuilderClass($this->connection); $instance->setFetchMode($this->getFetchMode(), $this->hydratorConstructorArgs); + + foreach ($tables as $table) { + $instance->getStatementCollection()->addTable(new TableStatement($table)); + } + + /** REMOVE BELOW HERE IN V0.2 */ $tables = $this->addTablePrefix($tables, false); $instance->addStatement('tables', $tables); + /** REMOVE ABOVE HERE IN V0.2 */ return $instance; } @@ -1562,4 +1570,13 @@ public function jsonBuilder(): JsonQueryBuilder { return new JsonQueryBuilder($this->getConnection(), $this->getFetchMode(), $this->hydratorConstructorArgs); } + + /** + * Get the value of statementCollection + * @return StatementCollection + */ + public function getStatementCollection(): StatementCollection + { + return $this->statementCollection; + } } diff --git a/src/QueryBuilder/WPDBAdapter.php b/src/QueryBuilder/WPDBAdapter.php index 24ee768..b6e2e21 100644 --- a/src/QueryBuilder/WPDBAdapter.php +++ b/src/QueryBuilder/WPDBAdapter.php @@ -50,7 +50,7 @@ public function selectCol(StatementCollection $col, $data, $statements) // @phps $col->addSelect(new SelectStatement('*')); } $_selects = $parser->parseSelect($col->getSelect()); - + $_tables = $parser->parseTable($col->getTable()); // From diff --git a/src/WpdbHandler.php b/src/WpdbHandler.php new file mode 100644 index 0000000..08d1120 --- /dev/null +++ b/src/WpdbHandler.php @@ -0,0 +1,70 @@ + + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @package Gin0115\Pixie + */ + +namespace Pixie; + +class WpdbHandler +{ + /** + * WPDB instance + * + * @var \wpdb + */ + protected $wpdb; + + /** + * Should wpdb errors be thrown as exceptions + * + * @var bool + */ + protected $throwErrors = false; + + public function __construct(Connection $connection) + { + $this->wpdb = $connection->getDbInstance(); + + $this->throwErrors = array_key_exists(Connection::THROW_ERRORS, $connection->getAdapterConfig()) + ? true === (bool) $connection->getAdapterConfig()[Connection::THROW_ERRORS] + : false; + } + + /** + * Uses WPDB::prepare() to interpolate the query passed. + + * + * @param string $query The sql query with parameter placeholders + * @param mixed[] $params The array of substitution parameters + * + * @return string The interpolated query + * + * @todo hook into catch on error. + */ + public function interpolateQuery(string $query, array $params = []): string + { + // Only call this when we have valid params (avoids wpdb::prepare() incorrectly called error) + $value = empty($params) ? $query : $this->wpdb->prepare($query, $params); + return is_string($value) ? $value : ''; + } +} diff --git a/tests/Unit/Parser/TestStatementParser.php b/tests/Unit/Parser/TestStatementParser.php index 8aa3e99..62fbff9 100644 --- a/tests/Unit/Parser/TestStatementParser.php +++ b/tests/Unit/Parser/TestStatementParser.php @@ -20,6 +20,7 @@ use Pixie\Tests\Logable_WPDB; use Pixie\Statement\Statement; use Pixie\Parser\StatementParser; +use Pixie\Statement\TableStatement; use Pixie\Tests\SQLAssertionsTrait; use Pixie\Statement\SelectStatement; use Pixie\Statement\StatementCollection; @@ -80,10 +81,7 @@ public function testSelectParserWithAcceptTypes(): void $this->assertStringContainsString('JSON_UNQUOTE(JSON_EXTRACT(jsSimpleCol, "$.nodeA.nodeB"))', $parsed); $this->assertStringContainsString('JSON_UNQUOTE(JSON_EXTRACT(pfx_table.jsSimpleCol, "$.nodeA.nodeB"))', $parsed); $this->assertStringContainsString('rawSimpleCol', $parsed); - $this->assertStringContainsString('pfx_table.rawSimpleCol', $parsed); - - // Verify as valid SQL. - $this->assertValidSQL(sprintf("SELECT %s FROM fooTable;", $parsed)); + $this->assertStringContainsString(' table.rawSimpleCol', $parsed); } /** @testdox Is should be possible to parse all expected select with aliases values from string, json arrow and selector objects and raw expressions and have them returned as a valid SQL fragment. */ @@ -111,9 +109,24 @@ public function testSelectParserWithAcceptTypesWithAliases(): void $this->assertStringContainsString('JSON_UNQUOTE(JSON_EXTRACT(jsSimpleCol, "$.nodeA.nodeB")) AS alias', $parsed); $this->assertStringContainsString('JSON_UNQUOTE(JSON_EXTRACT(egh_table.jsSimpleCol, "$.nodeA.nodeB")) AS alias', $parsed); $this->assertStringContainsString('rawSimpleCol AS alias', $parsed); - $this->assertStringContainsString('egh_table.rawSimpleCol', $parsed); + $this->assertStringContainsString(' table.rawSimpleCol AS alias', $parsed); // Should not add the prefix + } + + /** @testdox It should be possible to parse a table passes as either a string or raw expression. */ + public function testTableParserWithoutAliases(): void + { + // Without prefix + $collection = new StatementCollection(); + $collection->addTable(new TableStatement('string')); + $collection->addTable(new TableStatement(new Raw('raw(%s)', ['str']))); + + $parsed = $this->getParser()->parseTable($collection->getTable()); + $this->assertStringContainsString('string', $parsed); + $this->assertStringContainsString('raw(\'str\')', $parsed); - // Verify as valid SQL. - $this->assertValidSQL(sprintf("SELECT %s FROM fooTable;", $parsed)); + // With prefix + $parsed = $this->getParser([Connection::PREFIX => 'pfx_'])->parseTable($collection->getTable()); + $this->assertStringContainsString('pfx_string', $parsed); + $this->assertStringContainsString('raw(\'str\')', $parsed); } } From 71c5e30060cd6d0d56d6524a4cd43194923c986f Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Tue, 15 Feb 2022 00:10:48 +0000 Subject: [PATCH 10/24] House Keeping --- tests/Unit/TestRaw.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/Unit/TestRaw.php b/tests/Unit/TestRaw.php index fb5af42..0887ddd 100644 --- a/tests/Unit/TestRaw.php +++ b/tests/Unit/TestRaw.php @@ -40,4 +40,13 @@ public function testCanGetExpressionAndBindings(): void $this->assertContains(2, $raw->getBindings()); $this->assertContains(3, $raw->getBindings()); } + + /** @testdox It should be possible to quickly check if a raw statement has bindings applied. */ + public function testHasBindings(): void + { + $without = new Raw('a'); + $with = new Raw('b', [1,2,3]); + $this->assertFalse($without->hasBindings()); + $this->assertTrue($with->hasBindings()); + } } From 8940cab38cf7de74ce0a7fbfe96d40af851c09ce Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Wed, 16 Feb 2022 00:58:16 +0000 Subject: [PATCH 11/24] orderby now works with the flexible key value relationship. Helper method created --- src/Parser/Normalizer.php | 33 +++++++ src/Parser/StatementParser.php | 29 ++++++ src/QueryBuilder/QueryBuilderHandler.php | 69 ++++++++++++- src/QueryBuilder/WPDBAdapter.php | 28 ++---- src/Statement/OrderByStatement.php | 119 +++++++++++++++++++++++ src/Statement/SelectStatement.php | 2 + src/Statement/Statement.php | 1 + src/Statement/StatementCollection.php | 43 +++++++- tests/Unit/TestQueryBuilderHandler.php | 38 ++++++++ 9 files changed, 335 insertions(+), 27 deletions(-) create mode 100644 src/Statement/OrderByStatement.php diff --git a/src/Parser/Normalizer.php b/src/Parser/Normalizer.php index db6fe37..7a24ca1 100644 --- a/src/Parser/Normalizer.php +++ b/src/Parser/Normalizer.php @@ -34,6 +34,7 @@ use Pixie\Statement\TableStatement; use Pixie\Statement\SelectStatement; use Pixie\JSON\JsonExpressionFactory; +use Pixie\Statement\OrderByStatement; class Normalizer { @@ -111,6 +112,38 @@ public function selectStatement(SelectStatement $statement): string } } + /** + * Normalize all orderBy statements into strings + * + * Accepts string or (string) JSON Arrow Selectors, + * JsonSelector Objects and Raw Objects as fields. + * + * @param \Pixie\Statement\OrderByStatement $statement + * @return string + * @since 0.2.0 + */ + public function orderByStatement(OrderByStatement $statement): string + { + $field = $statement->getField(); + switch (true) { + // Is JSON Arrow Selector. + case is_string($field) && $this->jsonSelectors->isJsonSelector($field): + return $this->normalizeJsonArrowSelector($field); + + // If JSON selector + case is_a($field, JsonSelector::class): + return $this->normalizeJsonSelector($field); + + // RAW + case is_a($field, Raw::class): + return $this->normalizeRaw($field); + + // Assume fallback as string. + default: + return $this->tablePrefixer->field($field); + } + } + /** * Normalize all table states into strings * diff --git a/src/Parser/StatementParser.php b/src/Parser/StatementParser.php index 3dee23e..04eabe4 100644 --- a/src/Parser/StatementParser.php +++ b/src/Parser/StatementParser.php @@ -32,10 +32,12 @@ use Pixie\Statement\TableStatement; use Pixie\Statement\SelectStatement; use Pixie\JSON\JsonExpressionFactory; +use Pixie\Statement\OrderByStatement; class StatementParser { protected const TEMPLATE_AS = "%s AS %s"; + protected const TEMPLATE_ORDERBY = "ORDER BY %s"; /** * @var Connection @@ -117,4 +119,31 @@ public function parseTable(array $tables): string return join(', ', $tables); } + + /** + * Normalizes and Parsers an array of OrderByStatements. + * + * @param OrderByStatement[]|mixed[] $orderBy + * @return string + */ + public function parseOrderBy(array $orderBy): string + { + // Remove any none OrderByStatements + $orderBy = array_filter($orderBy, function ($statement): bool { + return is_a($statement, OrderByStatement::class); + }); + + // Cast to string, with or without alias, + $orderBy = array_map(function (OrderByStatement $value): string { + return sprintf( + "%s %s", + $this->normalizer->orderByStatement($value), + $value->getDirection() + ); + }, $orderBy); + + return 0 === count($orderBy) + ? '' + : sprintf(self::TEMPLATE_ORDERBY, join(', ', $orderBy)); + } } diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index 7107e43..7605916 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -9,8 +9,9 @@ use Pixie\Exception; use Pixie\Connection; -use Pixie\HasConnection; +use function mb_strlen; +use Pixie\HasConnection; use Pixie\JSON\JsonHandler; use Pixie\QueryBuilder\Raw; use Pixie\Hydration\Hydrator; @@ -21,8 +22,8 @@ use Pixie\Statement\TableStatement; use Pixie\Statement\SelectStatement; use Pixie\QueryBuilder\TablePrefixer; +use Pixie\Statement\OrderByStatement; use Pixie\Statement\StatementCollection; -use function mb_strlen; class QueryBuilderHandler implements HasConnection { @@ -816,7 +817,56 @@ public function groupBy($field): self } /** - * @param string|array $fields + * Will flip and array where the key should be an object. + * + * $columns = ['columnA' => 'aliasA', 'aliasB' => Raw::val('count(foo)'), 'noAlias']; + * $flipped = array_map([$this, 'maybeFlipArrayValues'], $columns); + * [ + * ['key' => 'columnA', value => 'aliasA'], + * ['key' => Raw::val('count(foo)'), value => 'aliasB'], + * ['key' => 'noAlias', 'value'=> 2 ] + * ] + * + * @param string|int $key + * @param string|object|int $value + * @return array{key:string|object|int,value:string|object|int} + */ + public function _maybeFlipArrayValues($key, $value): array + { + return is_object($value) || is_int($key) + ? ['key' => $value, 'value' => $key] + : ['key' => $key, 'value' => $value]; + } + + /** + * Will flip and array where the key should be an object. + * + * $columns = ['columnA' => 'aliasA', 'aliasB' => Raw::val('count(foo)'), 'noAlias']; + * $flipped = array_map([$this, 'maybeFlipArrayValues'], $columns); + * [ + * ['key' => 'columnA', value => 'aliasA'], + * ['key' => Raw::val('count(foo)'), value => 'aliasB'], + * ['key' => 'noAlias', 'value'=> 2 ] + * ] + * + * @param array + * @return array{key:string|object|int,value:string|object|int} + */ + public function maybeFlipArrayValues(array $data): array + { + return array_map( + function ($key, $value): array { + return is_object($value) || is_int($key) + ? ['key' => $value, 'value' => $key] + : ['key' => $key, 'value' => $value]; + }, + array_keys($data), + array_values($data) + ); + } + + /** + * @param string|Raw|JsonSelector|array $fields * @param string $defaultDirection * * @return static @@ -827,6 +877,18 @@ public function orderBy($fields, string $defaultDirection = 'ASC'): self $fields = [$fields]; } + foreach ( + // Key = Column && Value = Direction + $this->maybeFlipArrayValues($fields) + as ["key" => $column, "value" => $direction] + ) { + $this->statementCollection->addOrderBy(new OrderByStatement( + $column, + is_int($direction) ? $defaultDirection : $direction + )); + } + + /** REMOVE BELOW HERE IN v0.2 */ foreach ($fields as $key => $value) { $field = $key; $type = $value; @@ -843,6 +905,7 @@ public function orderBy($fields, string $defaultDirection = 'ASC'): self $field = $this->addTablePrefix($field); } $this->statements['orderBys'][] = compact('field', 'type'); + /** REMOVE ABOVE HERE IN v0.2 */ } return $this; diff --git a/src/QueryBuilder/WPDBAdapter.php b/src/QueryBuilder/WPDBAdapter.php index b6e2e21..979d47b 100644 --- a/src/QueryBuilder/WPDBAdapter.php +++ b/src/QueryBuilder/WPDBAdapter.php @@ -51,12 +51,13 @@ public function selectCol(StatementCollection $col, $data, $statements) // @phps } $_selects = $parser->parseSelect($col->getSelect()); $_tables = $parser->parseTable($col->getTable()); + // dump($col); // From - $tables = $this->arrayStr($statements['tables'], ', '); - // Select - $selects = $this->arrayStr($statements['selects'], ', '); + // $tables = $this->arrayStr($statements['tables'], ', '); + // // Select + // $selects = $this->arrayStr($statements['selects'], ', '); // Wheres list($whereCriteria, $whereBindings) = $this->buildCriteriaWithType($statements, 'wheres', 'WHERE'); @@ -67,21 +68,6 @@ public function selectCol(StatementCollection $col, $data, $statements) // @phps $groupBys = 'GROUP BY ' . $groupBys; } - // Order bys - $orderBys = ''; - if (isset($statements['orderBys']) && is_array($statements['orderBys'])) { - foreach ($statements['orderBys'] as $orderBy) { - $field = $this->wrapSanitizer($orderBy['field']); - if ($field instanceof Closure) { - continue; - } - $orderBys .= $field . ' ' . $orderBy['type'] . ', '; - } - - if ($orderBys = trim($orderBys, ', ')) { - $orderBys = 'ORDER BY ' . $orderBys; - } - } // Limit and offset $limit = isset($statements['limit']) ? 'LIMIT ' . (int) $statements['limit'] : ''; @@ -96,14 +82,14 @@ public function selectCol(StatementCollection $col, $data, $statements) // @phps /** @var string[] */ $sqlArray = [ 'SELECT' . (isset($statements['distinct']) ? ' DISTINCT' : ''), - $selects, + $parser->parseSelect($col->getSelect()), 'FROM', - $tables, + $parser->parseTable($col->getTable()), $joinString, $whereCriteria, $groupBys, $havingCriteria, - $orderBys, + $parser->parseOrderBy($col->getOrderBy()), $limit, $offset, ]; diff --git a/src/Statement/OrderByStatement.php b/src/Statement/OrderByStatement.php new file mode 100644 index 0000000..5f666da --- /dev/null +++ b/src/Statement/OrderByStatement.php @@ -0,0 +1,119 @@ + + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @package Gin0115\Pixie + * @subpackage QueryBuilder\Statement + */ + +namespace Pixie\Statement; + +use TypeError; +use Pixie\QueryBuilder\Raw; +use Pixie\JSON\JsonSelector; +use Pixie\Statement\Statement; + +class OrderByStatement implements Statement +{ + /** + * The field which is being order by + * + * @var string|Raw|JsonSelector + */ + protected $field; + + /** + * The direction for the order by field + * + * @var string|null + */ + protected $direction = null; + + /** + * Creates a Select Statement + * + * @param string|Raw|JsonSelector $field + * @param string|null $direction + */ + public function __construct($field, ?string $direction = null) + { + // Verify valid field type. + $this->verifyField($field); + $this->field = $field; + $this->direction = $direction; + } + + /** @inheritDoc */ + public function getType(): string + { + return Statement::ORDERBY; + } + + /** + * Verifies if the passed filed is of a valid type. + * + * @param mixed $field + * @return void + */ + protected function verifyField($field): void + { + dump($field); + if ( + !is_string($field) + && ! is_a($field, Raw::class) + && !is_a($field, JsonSelector::class) + ) { + throw new TypeError("Only string, Raw and JsonSelectors may be used as orderBy fields"); + } + } + /** + * Gets the field. + * + * @return string|Raw|JsonSelector + */ + public function getField() + { + return $this->field; + } + + /** + * Checks if we have a defined direction + * + * @return bool + */ + public function hasDirection(): bool + { + return is_string($this->direction) + && in_array(strtoupper($this->direction), ['ASC', 'DESC']); + } + + /** + * Gets the direction + * + * @return string|null + */ + public function getDirection(): ?string + { + return $this->hasDirection() + ? \strtoupper($this->direction) + : null; + } +} diff --git a/src/Statement/SelectStatement.php b/src/Statement/SelectStatement.php index fa1e2ae..48c93ee 100644 --- a/src/Statement/SelectStatement.php +++ b/src/Statement/SelectStatement.php @@ -88,6 +88,7 @@ protected function verifyField($field): void * Checks if the passed field needs to be interpolated. * * @return bool TRUE if Raw or JsonSelector, FALSE if string. + * @todo REMOVE ME */ public function fieldRequiresInterpolation(): bool { @@ -100,6 +101,7 @@ public function fieldRequiresInterpolation(): bool * @psalm-immutable * @param \Closure(string|Raw|JsonSelector $field): string $callback * @return SelectStatement + * @todo REMOVE ME */ public function interpolateField(\Closure $callback): SelectStatement { diff --git a/src/Statement/Statement.php b/src/Statement/Statement.php index 0adb2e4..79edc0b 100644 --- a/src/Statement/Statement.php +++ b/src/Statement/Statement.php @@ -33,6 +33,7 @@ interface Statement */ public const SELECT = 'select'; public const TABLE = 'table'; + public const ORDERBY = 'orderby'; /** * Get the statement type diff --git a/src/Statement/StatementCollection.php b/src/Statement/StatementCollection.php index 96be253..26e554d 100644 --- a/src/Statement/StatementCollection.php +++ b/src/Statement/StatementCollection.php @@ -33,11 +33,16 @@ class StatementCollection /** * Holds all the statements * - * @var array{select:SelectStatement[],table:TableStatement[]} + * @var array{ + * select: SelectStatement[], + * table: TableStatement[], + * orderby: OrderByStatement[] + * } */ protected $statements = [ - Statement::SELECT => [], - Statement::TABLE => [], + Statement::SELECT => [], + Statement::TABLE => [], + Statement::ORDERBY => [] ]; /** @@ -113,4 +118,36 @@ public function hasTable(): bool { return 0 < count($this->getTable()); } + + /** + * Adds a select statement to the collection. + * + * @param OrderByStatement $statement + * @return self + */ + public function addOrderBy(OrderByStatement $statement): self + { + $this->statements[Statement::ORDERBY][] = $statement; + return $this; + } + + /** + * Get all OrderBy Statements + * + * @return OrderByStatement[] + */ + public function getOrderBy(): array + { + return $this->statements[Statement::ORDERBY]; + } + + /** + * OrderBy statements exist. + * + * @return bool + */ + public function hasOrderBy(): bool + { + return 0 < count($this->getOrderBy()); + } } diff --git a/tests/Unit/TestQueryBuilderHandler.php b/tests/Unit/TestQueryBuilderHandler.php index 49becfa..8f10e33 100644 --- a/tests/Unit/TestQueryBuilderHandler.php +++ b/tests/Unit/TestQueryBuilderHandler.php @@ -15,6 +15,7 @@ use WP_UnitTestCase; use Pixie\Connection; use Pixie\QueryBuilder\Raw; +use Pixie\JSON\JsonSelector; use Pixie\Tests\Logable_WPDB; use Pixie\QueryBuilder\Transaction; use Pixie\Exception as PixieException; @@ -277,4 +278,41 @@ public function testCanCreateJSONBuilder(): void $this->assertSame($inital->getConnection(), $json->getConnection()); $this->assertSame($inital->getFetchMode(), $json->getFetchMode()); } + + public function testMaybeFlipArrayValues(): void + { + $builder = $this->queryBuilderProvider(); + $columns = ['columnA' => 'aliasA', 'aliasB' => Raw::val('count(foo)'), 'noAlias']; + $flipped = array_map([$builder, 'maybeFlipArrayValues'], array_keys($columns), array_values($columns)); + // dump($flipped); + // [ + // ['key' => 'columnA', value => 'aliasA'], + // ['key' => Raw::val('count(foo)'), value => 'aliasB'], + // ['key' => 'noAlias', 'value'=> 2 ] + // ] + } + + /** @testdox When creating an orderby statement, they could be passed as [COLUMN{string} => DIRECTION] but as [DIRECTION => COLUMN{object}] */ + public function testOrderByFlipValuesAsOrderBys(): void + { + $builder = $this->queryBuilderProvider(); + $builder->orderBy(['column' => 'ASC']); + $builder->orderBy(Raw::val('no dire'), 'DESC'); + $builder->orderBy(['ASC' => Raw::val('test')]); + $builder->orderBy(['DESC' => new JsonSelector('col', ['nod1', 'nod2'])]); + + $statements = $builder->getStatementCollection()->getOrderBy(); + dump($statements); + $this->assertEquals('column', $statements[0]->getField()); + $this->assertEquals('ASC', $statements[0]->getDirection()); + + $this->assertInstanceOf(Raw::class, $statements[1]->getField()); + $this->assertEquals('DESC', $statements[1]->getDirection()); + + $this->assertInstanceOf(Raw::class, $statements[2]->getField()); + $this->assertEquals('ASC', $statements[2]->getDirection()); + + $this->assertInstanceOf(JsonSelector::class, $statements[3]->getField()); + $this->assertEquals('DESC', $statements[3]->getDirection()); + } } From 177d0711eb6a14e3603fae2defcdfeb68edb6caa Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Wed, 16 Feb 2022 01:14:19 +0000 Subject: [PATCH 12/24] House Keeping --- src/QueryBuilder/QueryBuilderHandler.php | 15 ++++++++------- src/Statement/OrderByStatement.php | 2 +- tests/Unit/TestQueryBuilderHandler.php | 21 ++++++++++++--------- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index 7605916..5f2ebe1 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -14,6 +14,7 @@ use Pixie\HasConnection; use Pixie\JSON\JsonHandler; use Pixie\QueryBuilder\Raw; +use Pixie\JSON\JsonSelector; use Pixie\Hydration\Hydrator; use Pixie\QueryBuilder\JoinBuilder; use Pixie\QueryBuilder\QueryObject; @@ -846,11 +847,11 @@ public function _maybeFlipArrayValues($key, $value): array * [ * ['key' => 'columnA', value => 'aliasA'], * ['key' => Raw::val('count(foo)'), value => 'aliasB'], - * ['key' => 'noAlias', 'value'=> 2 ] + * ['key' => 'noAlias', 'value'=> 0 ] * ] * - * @param array - * @return array{key:string|object|int,value:string|object|int} + * @param array $data + * @return array{key:string|object|int,value:string|object|int}[] */ public function maybeFlipArrayValues(array $data): array { @@ -883,8 +884,8 @@ public function orderBy($fields, string $defaultDirection = 'ASC'): self as ["key" => $column, "value" => $direction] ) { $this->statementCollection->addOrderBy(new OrderByStatement( - $column, - is_int($direction) ? $defaultDirection : $direction + $column, // @phpstan-ignore-line + is_int($direction) ? $defaultDirection : (string) $direction )); } @@ -898,11 +899,11 @@ public function orderBy($fields, string $defaultDirection = 'ASC'): self } if ($this->jsonHandler->isJsonSelector($field)) { - $field = $this->jsonHandler->extractAndUnquoteFromJsonSelector($field); + $field = $this->jsonHandler->extractAndUnquoteFromJsonSelector($field); // @phpstan-ignore-line } if (!$field instanceof Raw) { - $field = $this->addTablePrefix($field); + $field = $this->addTablePrefix($field); // @phpstan-ignore-line } $this->statements['orderBys'][] = compact('field', 'type'); /** REMOVE ABOVE HERE IN v0.2 */ diff --git a/src/Statement/OrderByStatement.php b/src/Statement/OrderByStatement.php index 5f666da..9034b04 100644 --- a/src/Statement/OrderByStatement.php +++ b/src/Statement/OrderByStatement.php @@ -113,7 +113,7 @@ public function hasDirection(): bool public function getDirection(): ?string { return $this->hasDirection() - ? \strtoupper($this->direction) + ? \strtoupper($this->direction ?? '') : null; } } diff --git a/tests/Unit/TestQueryBuilderHandler.php b/tests/Unit/TestQueryBuilderHandler.php index 8f10e33..08f10b6 100644 --- a/tests/Unit/TestQueryBuilderHandler.php +++ b/tests/Unit/TestQueryBuilderHandler.php @@ -279,17 +279,21 @@ public function testCanCreateJSONBuilder(): void $this->assertSame($inital->getFetchMode(), $json->getFetchMode()); } + /** @testdox It should be possible to cast an array to VALUE with ADDITIONAL as [VALUE => ADDITIONAL], but reversed if the value is an object [ADDITIONAL => VALUE{}] or no key used [VALUE] */ public function testMaybeFlipArrayValues(): void { $builder = $this->queryBuilderProvider(); - $columns = ['columnA' => 'aliasA', 'aliasB' => Raw::val('count(foo)'), 'noAlias']; - $flipped = array_map([$builder, 'maybeFlipArrayValues'], array_keys($columns), array_values($columns)); - // dump($flipped); - // [ - // ['key' => 'columnA', value => 'aliasA'], - // ['key' => Raw::val('count(foo)'), value => 'aliasB'], - // ['key' => 'noAlias', 'value'=> 2 ] - // ] + $raw = Raw::val('count(foo)'); + $columns = [ + 'columnA' => 'aliasA', + 'aliasB' => $raw, + 'noAlias' + ]; + $flipped = $builder->maybeFlipArrayValues($columns); + + $this->assertContains(['key' => 'columnA', 'value' => 'aliasA'], $flipped); + $this->assertContains(['key' => $raw, 'value' => 'aliasB'], $flipped); + $this->assertContains(['key' => 'noAlias', 'value' => 0 ], $flipped); } /** @testdox When creating an orderby statement, they could be passed as [COLUMN{string} => DIRECTION] but as [DIRECTION => COLUMN{object}] */ @@ -302,7 +306,6 @@ public function testOrderByFlipValuesAsOrderBys(): void $builder->orderBy(['DESC' => new JsonSelector('col', ['nod1', 'nod2'])]); $statements = $builder->getStatementCollection()->getOrderBy(); - dump($statements); $this->assertEquals('column', $statements[0]->getField()); $this->assertEquals('ASC', $statements[0]->getDirection()); From 454a67dcc7545c9fa856f716106146f1620c1645 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Wed, 16 Feb 2022 01:21:11 +0000 Subject: [PATCH 13/24] House Keeping --- src/QueryBuilder/QueryBuilderHandler.php | 4 ++-- src/QueryBuilder/WPDBAdapter.php | 1 - src/Statement/OrderByStatement.php | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index 5f2ebe1..f87695e 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -9,9 +9,8 @@ use Pixie\Exception; use Pixie\Connection; -use function mb_strlen; - use Pixie\HasConnection; + use Pixie\JSON\JsonHandler; use Pixie\QueryBuilder\Raw; use Pixie\JSON\JsonSelector; @@ -25,6 +24,7 @@ use Pixie\QueryBuilder\TablePrefixer; use Pixie\Statement\OrderByStatement; use Pixie\Statement\StatementCollection; +use function mb_strlen; class QueryBuilderHandler implements HasConnection { diff --git a/src/QueryBuilder/WPDBAdapter.php b/src/QueryBuilder/WPDBAdapter.php index 979d47b..439aaca 100644 --- a/src/QueryBuilder/WPDBAdapter.php +++ b/src/QueryBuilder/WPDBAdapter.php @@ -51,7 +51,6 @@ public function selectCol(StatementCollection $col, $data, $statements) // @phps } $_selects = $parser->parseSelect($col->getSelect()); $_tables = $parser->parseTable($col->getTable()); - // dump($col); // From diff --git a/src/Statement/OrderByStatement.php b/src/Statement/OrderByStatement.php index 9034b04..e49a17f 100644 --- a/src/Statement/OrderByStatement.php +++ b/src/Statement/OrderByStatement.php @@ -75,7 +75,6 @@ public function getType(): string */ protected function verifyField($field): void { - dump($field); if ( !is_string($field) && ! is_a($field, Raw::class) From 92a6c19cdf24401cfd60c88a4605524d0ea7f63c Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Wed, 16 Feb 2022 01:37:38 +0000 Subject: [PATCH 14/24] added in groupby with basic tests --- src/Parser/StatementParser.php | 29 ++++++++- src/QueryBuilder/QueryBuilderHandler.php | 8 +++ src/QueryBuilder/WPDBAdapter.php | 2 +- src/Statement/GroupByStatement.php | 83 ++++++++++++++++++++++++ src/Statement/OrderByStatement.php | 2 +- src/Statement/Statement.php | 3 +- src/Statement/StatementCollection.php | 46 +++++++++++-- 7 files changed, 162 insertions(+), 11 deletions(-) create mode 100644 src/Statement/GroupByStatement.php diff --git a/src/Parser/StatementParser.php b/src/Parser/StatementParser.php index 04eabe4..17bfb09 100644 --- a/src/Parser/StatementParser.php +++ b/src/Parser/StatementParser.php @@ -32,12 +32,14 @@ use Pixie\Statement\TableStatement; use Pixie\Statement\SelectStatement; use Pixie\JSON\JsonExpressionFactory; +use Pixie\Statement\GroupByStatement; use Pixie\Statement\OrderByStatement; class StatementParser { protected const TEMPLATE_AS = "%s AS %s"; - protected const TEMPLATE_ORDERBY = "ORDER BY %s"; + protected const TEMPLATE_ORDER_BY = "ORDER BY %s"; + protected const TEMPLATE_GROUP_BY = "GROUP BY %s"; /** * @var Connection @@ -144,6 +146,29 @@ public function parseOrderBy(array $orderBy): string return 0 === count($orderBy) ? '' - : sprintf(self::TEMPLATE_ORDERBY, join(', ', $orderBy)); + : sprintf(self::TEMPLATE_ORDER_BY, join(', ', $orderBy)); + } + + /** + * Normalizes and Parsers an array of GroupByStatements. + * + * @param GroupByStatement[]|mixed[] $orderBy + * @return string + */ + public function parseGroupBy(array $orderBy): string + { + // Remove any none GroupByStatements + $orderBy = array_filter($orderBy, function ($statement): bool { + return is_a($statement, GroupByStatement::class); + }); + + // Get the array of columns. + $orderBy = array_map(function (GroupByStatement $value): string { + return $value->getField(); + }, $orderBy); + + return 0 === count($orderBy) + ? '' + : sprintf(self::TEMPLATE_GROUP_BY, join(', ', $orderBy)); } } diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index f87695e..9e8fa0b 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -22,6 +22,7 @@ use Pixie\Statement\TableStatement; use Pixie\Statement\SelectStatement; use Pixie\QueryBuilder\TablePrefixer; +use Pixie\Statement\GroupByStatement; use Pixie\Statement\OrderByStatement; use Pixie\Statement\StatementCollection; use function mb_strlen; @@ -811,8 +812,15 @@ public function selectDistinct($fields) */ public function groupBy($field): self { + $groupBys = is_array($field) ? $field : [$field]; + foreach (array_filter($groupBys, 'is_string') as $groupBy) { + $this->statementCollection->addGroupBy(new GroupByStatement($groupBy)); + } + + /** REMOVE BELOW IN V0.2 */ $field = $this->addTablePrefix($field); $this->addStatement('groupBys', $field); + /** REMOVE ABOVE IN V0.2 */ return $this; } diff --git a/src/QueryBuilder/WPDBAdapter.php b/src/QueryBuilder/WPDBAdapter.php index 439aaca..5e74145 100644 --- a/src/QueryBuilder/WPDBAdapter.php +++ b/src/QueryBuilder/WPDBAdapter.php @@ -86,7 +86,7 @@ public function selectCol(StatementCollection $col, $data, $statements) // @phps $parser->parseTable($col->getTable()), $joinString, $whereCriteria, - $groupBys, + $parser->parseGroupBy($col->getGroupBy()), $havingCriteria, $parser->parseOrderBy($col->getOrderBy()), $limit, diff --git a/src/Statement/GroupByStatement.php b/src/Statement/GroupByStatement.php new file mode 100644 index 0000000..1dfd459 --- /dev/null +++ b/src/Statement/GroupByStatement.php @@ -0,0 +1,83 @@ + + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @package Gin0115\Pixie + * @subpackage QueryBuilder\Statement + */ + +namespace Pixie\Statement; + +use TypeError; +use Pixie\Statement\Statement; + +class GroupByStatement implements Statement +{ + /** + * The field which is being group by + * + * @var string + */ + protected $field; + + + /** + * Creates a Select Statement + * + * @param string $field + */ + public function __construct(string $field) + { + // Verify valid field type. + $this->verifyField($field); + $this->field = $field; + } + + /** @inheritDoc */ + public function getType(): string + { + return Statement::ORDER_BY; + } + + /** + * Verifies if the passed filed is of a valid type. + * + * @param mixed $field + * @return void + */ + protected function verifyField($field): void + { + if ( + !is_string($field) + ) { + throw new TypeError("Only string may be used as group by fields"); + } + } + /** + * Gets the field. + * + * @return string + */ + public function getField(): string + { + return $this->field; + } +} diff --git a/src/Statement/OrderByStatement.php b/src/Statement/OrderByStatement.php index e49a17f..6fa5abf 100644 --- a/src/Statement/OrderByStatement.php +++ b/src/Statement/OrderByStatement.php @@ -64,7 +64,7 @@ public function __construct($field, ?string $direction = null) /** @inheritDoc */ public function getType(): string { - return Statement::ORDERBY; + return Statement::ORDER_BY; } /** diff --git a/src/Statement/Statement.php b/src/Statement/Statement.php index 79edc0b..1dc733c 100644 --- a/src/Statement/Statement.php +++ b/src/Statement/Statement.php @@ -33,7 +33,8 @@ interface Statement */ public const SELECT = 'select'; public const TABLE = 'table'; - public const ORDERBY = 'orderby'; + public const ORDER_BY = 'orderby'; + public const GROUP_BY = 'groupby'; /** * Get the statement type diff --git a/src/Statement/StatementCollection.php b/src/Statement/StatementCollection.php index 26e554d..085b311 100644 --- a/src/Statement/StatementCollection.php +++ b/src/Statement/StatementCollection.php @@ -36,13 +36,15 @@ class StatementCollection * @var array{ * select: SelectStatement[], * table: TableStatement[], - * orderby: OrderByStatement[] + * orderby: OrderByStatement[], + * groupby: GroupByStatement[] * } */ protected $statements = [ Statement::SELECT => [], Statement::TABLE => [], - Statement::ORDERBY => [] + Statement::ORDER_BY => [], + Statement::GROUP_BY => [], ]; /** @@ -88,7 +90,7 @@ public function hasSelect(): bool } /** - * Adds a select statement to the collection. + * Adds a table statement to the collection. * * @param TableStatement $statement * @return self @@ -120,14 +122,14 @@ public function hasTable(): bool } /** - * Adds a select statement to the collection. + * Adds a OrderBy statement to the collection. * * @param OrderByStatement $statement * @return self */ public function addOrderBy(OrderByStatement $statement): self { - $this->statements[Statement::ORDERBY][] = $statement; + $this->statements[Statement::ORDER_BY][] = $statement; return $this; } @@ -138,7 +140,7 @@ public function addOrderBy(OrderByStatement $statement): self */ public function getOrderBy(): array { - return $this->statements[Statement::ORDERBY]; + return $this->statements[Statement::ORDER_BY]; } /** @@ -150,4 +152,36 @@ public function hasOrderBy(): bool { return 0 < count($this->getOrderBy()); } + + /** + * Adds a GroupBy statement to the collection. + * + * @param GroupByStatement $statement + * @return self + */ + public function addGroupBy(GroupByStatement $statement): self + { + $this->statements[Statement::GROUP_BY][] = $statement; + return $this; + } + + /** + * Get all GroupBy Statements + * + * @return GroupByStatement[] + */ + public function getGroupBy(): array + { + return $this->statements[Statement::GROUP_BY]; + } + + /** + * GroupBy statements exist. + * + * @return bool + */ + public function hasGroupBy(): bool + { + return 0 < count($this->getGroupBy()); + } } From 85d40152f8bccfc39faa5a8d590ca37c947082d5 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Wed, 16 Feb 2022 22:12:42 +0000 Subject: [PATCH 15/24] House Keeping --- src/QueryBuilder/QueryBuilderHandler.php | 79 +++++++++++++------ src/QueryBuilder/WPDBAdapter.php | 19 +++-- src/Statement/SelectStatement.php | 29 +++++++ src/Statement/StatementCollection.php | 17 ++++ .../TestQueryBuilderSQLGeneration.php | 2 +- 5 files changed, 109 insertions(+), 37 deletions(-) diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index 9e8fa0b..fa9e5bb 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -9,8 +9,9 @@ use Pixie\Exception; use Pixie\Connection; -use Pixie\HasConnection; +use function mb_strlen; +use Pixie\HasConnection; use Pixie\JSON\JsonHandler; use Pixie\QueryBuilder\Raw; use Pixie\JSON\JsonSelector; @@ -25,7 +26,6 @@ use Pixie\Statement\GroupByStatement; use Pixie\Statement\OrderByStatement; use Pixie\Statement\StatementCollection; -use function mb_strlen; class QueryBuilderHandler implements HasConnection { @@ -533,9 +533,11 @@ public function getQuery(string $type = 'select', $dataToBePassed = []) if ('select' === $type) { $queryArr = $this->adapterInstance->selectCol($this->statementCollection, [], $this->statements); + } else { + $queryArr = $this->adapterInstance->$type($this->statements, $dataToBePassed); + } - $queryArr = $this->adapterInstance->$type($this->statements, $dataToBePassed); return new QueryObject($queryArr['sql'], $queryArr['bindings'], $this->dbInstance); } @@ -749,26 +751,66 @@ public function from(...$tables): self * Select which fields should be returned in the results. * * @param string|string[]|Raw[]|array $fields - * * @return static */ public function select($fields): self { - if (!is_array($fields)) { - $fields = func_get_args(); - } + // if (!is_array($fields)) { + // $fields = func_get_args(); + // } + $this->selectHandler(!is_array($fields) ? func_get_args() : $fields); + return $this; + } - foreach ($fields as $field => $alias) { + /** + * @param string|string[]|Raw[]|array $fields + * + * @return static + */ + public function selectDistinct($fields) + { + // $this->select($fields, true); + // $this->addStatement('distinct', true); + $this->selectHandler( + !is_array($fields) ? func_get_args() : $fields, + true + ); + return $this; + } + + private function selectHandler(array $selects, bool $isDistinct = false): void + { + $selects2 = $this->maybeFlipArrayValues($selects); + foreach ($selects2 as ["key" => $field, "value" => $alias]) { // If no alias passed, but field is for JSON. thrown an exception. if (is_numeric($field) && is_string($alias) && $this->jsonHandler->isJsonSelector($alias)) { throw new Exception("An alias must be used if you wish to select from JSON Object", 1); } /** V0.2 */ - $statement = is_numeric($field) - ? new SelectStatement($alias) + $statement = is_numeric($alias) + ? new SelectStatement($field) : new SelectStatement($field, $alias); - $this->statementCollection->addSelect($statement); + $this->statementCollection->addSelect( + $statement->setIsDistinct($isDistinct) + ); + } + + + + foreach ($selects as $field => $alias) { + // If no alias passed, but field is for JSON. thrown an exception. + if (is_numeric($field) && is_string($alias) && $this->jsonHandler->isJsonSelector($alias)) { + throw new Exception("An alias must be used if you wish to select from JSON Object", 1); + } + + // /** V0.2 */ + // $statement = is_numeric($field) + // ? new SelectStatement($alias) + // : new SelectStatement($field, $alias); + // $this->statementCollection->addSelect( + // $statement/* ->setIsDistinct($isDistinct) */ + // ); /** REMOVE BELOW IN V0.2 */ // If we have a JSON expression @@ -788,21 +830,6 @@ public function select($fields): self $this->addStatement('selects', $field); /** REMOVE ABOVE IN V0.2 */ } - - return $this; - } - - /** - * @param string|string[]|Raw[]|array $fields - * - * @return static - */ - public function selectDistinct($fields) - { - $this->select($fields); - $this->addStatement('distinct', true); - - return $this; } /** diff --git a/src/QueryBuilder/WPDBAdapter.php b/src/QueryBuilder/WPDBAdapter.php index 5e74145..e1c622c 100644 --- a/src/QueryBuilder/WPDBAdapter.php +++ b/src/QueryBuilder/WPDBAdapter.php @@ -6,17 +6,17 @@ use Pixie\Binding; use Pixie\Exception; +use function is_bool; + use Pixie\Connection; -use Pixie\QueryBuilder\Raw; +use function is_float; +use Pixie\QueryBuilder\Raw; use Pixie\Parser\StatementParser; - use Pixie\Statement\SelectStatement; use Pixie\QueryBuilder\NestedCriteria; use Pixie\Statement\StatementCollection; -use function is_bool; -use function is_float; class WPDBAdapter { @@ -62,10 +62,10 @@ public function selectCol(StatementCollection $col, $data, $statements) // @phps list($whereCriteria, $whereBindings) = $this->buildCriteriaWithType($statements, 'wheres', 'WHERE'); // Group bys - $groupBys = ''; - if (isset($statements['groupBys']) && $groupBys = $this->arrayStr($statements['groupBys'], ', ')) { - $groupBys = 'GROUP BY ' . $groupBys; - } + // $groupBys = ''; + // if (isset($statements['groupBys']) && $groupBys = $this->arrayStr($statements['groupBys'], ', ')) { + // $groupBys = 'GROUP BY ' . $groupBys; + // } // Limit and offset @@ -80,7 +80,7 @@ public function selectCol(StatementCollection $col, $data, $statements) // @phps /** @var string[] */ $sqlArray = [ - 'SELECT' . (isset($statements['distinct']) ? ' DISTINCT' : ''), + 'SELECT' . ($col->hasDistinctSelect() ? ' DISTINCT' : ''), $parser->parseSelect($col->getSelect()), 'FROM', $parser->parseTable($col->getTable()), @@ -94,7 +94,6 @@ public function selectCol(StatementCollection $col, $data, $statements) // @phps ]; $sql = $this->concatenateQuery($sqlArray); - $bindings = array_merge( $whereBindings, $havingBindings diff --git a/src/Statement/SelectStatement.php b/src/Statement/SelectStatement.php index 48c93ee..327ad9c 100644 --- a/src/Statement/SelectStatement.php +++ b/src/Statement/SelectStatement.php @@ -47,6 +47,13 @@ class SelectStatement implements Statement */ protected $alias = null; + /** + * Denotes if the select is distinct. + * + * @var bool + */ + protected $isDistinct = false; + /** * Creates a Select Statement * @@ -138,4 +145,26 @@ public function getAlias(): ?string { return $this->hasAlias() ? $this->alias : null; } + + /** + * Get denotes if the select is distinct. + * + * @return bool + */ + public function getIsDistinct(): bool + { + return $this->isDistinct; + } + + /** + * Set denotes if the select is distinct. + * + * @param bool $isDistinct Denotes if the select is distinct. + * @return self + */ + public function setIsDistinct(bool $isDistinct = true): self + { + $this->isDistinct = $isDistinct; + return $this; + } } diff --git a/src/Statement/StatementCollection.php b/src/Statement/StatementCollection.php index 085b311..fed41b5 100644 --- a/src/Statement/StatementCollection.php +++ b/src/Statement/StatementCollection.php @@ -89,6 +89,23 @@ public function hasSelect(): bool return 0 < count($this->getSelect()); } + /** + * Check if any defined select queries are distinct. + * + * @return bool + */ + public function hasDistinctSelect(): bool + { + $distinctSelects = array_filter( + $this->getSelect(), + function (SelectStatement $select): bool { + return $select->getIsDistinct(); + } + ); + + return 0 < count($distinctSelects); + } + /** * Adds a table statement to the collection. * diff --git a/tests/QueryBuilderHandler/TestQueryBuilderSQLGeneration.php b/tests/QueryBuilderHandler/TestQueryBuilderSQLGeneration.php index 91fd91e..ab081ad 100644 --- a/tests/QueryBuilderHandler/TestQueryBuilderSQLGeneration.php +++ b/tests/QueryBuilderHandler/TestQueryBuilderSQLGeneration.php @@ -1059,7 +1059,7 @@ public function testSubQueryInOperatorExample() $builder = $this->queryBuilderProvider(); $avgSubQuery = $builder->table('orders')->selectDistinct("customerNumber"); - + $builder->table('customers') ->select('customerName') ->whereNotIn('customerNumber', $builder->subQuery($avgSubQuery)) From c64f3afce10490bca0e31b57223d9655fe2d1da0 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Wed, 16 Feb 2022 23:39:23 +0000 Subject: [PATCH 16/24] House Keeping --- src/QueryBuilder/QueryBuilderHandler.php | 177 +++++++++++++---------- src/QueryBuilder/WPDBAdapter.php | 8 +- 2 files changed, 106 insertions(+), 79 deletions(-) diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index fa9e5bb..0e5c477 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -9,9 +9,8 @@ use Pixie\Exception; use Pixie\Connection; -use function mb_strlen; - use Pixie\HasConnection; + use Pixie\JSON\JsonHandler; use Pixie\QueryBuilder\Raw; use Pixie\JSON\JsonSelector; @@ -26,6 +25,7 @@ use Pixie\Statement\GroupByStatement; use Pixie\Statement\OrderByStatement; use Pixie\Statement\StatementCollection; +use function mb_strlen; class QueryBuilderHandler implements HasConnection { @@ -42,7 +42,7 @@ class QueryBuilderHandler implements HasConnection /** * @var array */ - protected $statements = []; + protected $statements = array(); /** @var StatementCollection */ protected $statementCollection; @@ -134,8 +134,8 @@ final public function __construct( */ protected function setAdapterConfig(array $adapterConfig): void { - if (isset($adapterConfig[Connection::PREFIX])) { - $this->tablePrefix = $adapterConfig[Connection::PREFIX]; + if (isset($adapterConfig[ Connection::PREFIX ])) { + $this->tablePrefix = $adapterConfig[ Connection::PREFIX ]; } } @@ -205,7 +205,7 @@ protected function constructCurrentBuilderClass(Connection $connection): self * @param array $bindings * @return string */ - public function interpolateQuery(string $query, array $bindings = []): string + public function interpolateQuery(string $query, array $bindings = array()): string { return $this->adapterInstance->interpolateQuery($query, $bindings); } @@ -216,7 +216,7 @@ public function interpolateQuery(string $query, array $bindings = []): string * * @return static */ - public function query($sql, $bindings = []): self + public function query($sql, $bindings = array()): self { list($this->sqlStatement) = $this->statement($sql, $bindings); @@ -229,16 +229,16 @@ public function query($sql, $bindings = []): self * * @return array{0:string, 1:float} */ - public function statement(string $sql, $bindings = []): array + public function statement(string $sql, $bindings = array()): array { $start = microtime(true); $sqlStatement = empty($bindings) ? $sql : $this->interpolateQuery($sql, $bindings); - if (!is_string($sqlStatement)) { + if (! is_string($sqlStatement)) { throw new Exception('Could not interpolate query', 1); } - return [$sqlStatement, microtime(true) - $start]; + return array( $sqlStatement, microtime(true) - $start ); } /** @@ -251,7 +251,7 @@ public function statement(string $sql, $bindings = []): array public function get() { $eventResult = $this->fireEvents('before-select'); - if (!is_null($eventResult)) { + if (! is_null($eventResult)) { return $eventResult; } $executionTime = 0; @@ -272,12 +272,12 @@ public function get() // If we are using the hydrator, return as OBJECT and let the hydrator map the correct model. $this->useHydrator() ? OBJECT : $this->getFetchMode() ); - $executionTime += microtime(true) - $start; + $executionTime += microtime(true) - $start; $this->sqlStatement = null; // Ensure we have an array of results. - if (!is_array($result) && null !== $result) { - $result = [$result]; + if (! is_array($result) && null !== $result) { + $result = array( $result ); } // Maybe hydrate the results. @@ -297,7 +297,7 @@ public function get() */ protected function getHydrator(): Hydrator /* @phpstan-ignore-line */ { - $hydrator = new Hydrator($this->getFetchMode(), $this->hydratorConstructorArgs ?? []); /* @phpstan-ignore-line */ + $hydrator = new Hydrator($this->getFetchMode(), $this->hydratorConstructorArgs ?? array()); /* @phpstan-ignore-line */ return $hydrator; } @@ -309,7 +309,7 @@ protected function getHydrator(): Hydrator /* @phpstan-ignore-line */ */ protected function useHydrator(): bool { - return !in_array($this->getFetchMode(), [\ARRAY_A, \ARRAY_N, \OBJECT, \OBJECT_K]); + return ! in_array($this->getFetchMode(), array( \ARRAY_A, \ARRAY_N, \OBJECT, \OBJECT_K )); } /** @@ -423,17 +423,16 @@ protected function aggregate(string $type, $field = '*'): float throw new \Exception(sprintf('Failed %s query - the column %s hasn\'t been selected in the query.', $type, $field)); } - if (false === isset($this->statements['tables'])) { throw new Exception('No table selected'); } $count = $this ->table($this->subQuery($this, 'count')) - ->select([$this->raw(sprintf('%s(%s) AS field', strtoupper($type), $field))]) + ->select(array( $this->raw(sprintf('%s(%s) AS field', strtoupper($type), $field)) )) ->first(); - return true === isset($count->field) ? (float)$count->field : 0; + return true === isset($count->field) ? (float) $count->field : 0; } /** @@ -449,7 +448,7 @@ protected function aggregate(string $type, $field = '*'): float */ public function count($field = '*'): int { - return (int)$this->aggregate('count', $field); + return (int) $this->aggregate('count', $field); } /** @@ -524,21 +523,19 @@ public function max($field): float * * @throws Exception */ - public function getQuery(string $type = 'select', $dataToBePassed = []) + public function getQuery(string $type = 'select', $dataToBePassed = array()) { - $allowedTypes = ['select', 'insert', 'insertignore', 'replace', 'delete', 'update', 'criteriaonly']; - if (!in_array(strtolower($type), $allowedTypes)) { + $allowedTypes = array( 'select', 'insert', 'insertignore', 'replace', 'delete', 'update', 'criteriaonly' ); + if (! in_array(strtolower($type), $allowedTypes)) { throw new Exception($type . ' is not a known type.', 2); } if ('select' === $type) { - $queryArr = $this->adapterInstance->selectCol($this->statementCollection, [], $this->statements); + $queryArr = $this->adapterInstance->selectCol($this->statementCollection, array(), $this->statements); } else { $queryArr = $this->adapterInstance->$type($this->statements, $dataToBePassed); - } - return new QueryObject($queryArr['sql'], $queryArr['bindings'], $this->dbInstance); } @@ -569,12 +566,12 @@ public function subQuery(QueryBuilderHandler $queryBuilder, ?string $alias = nul private function doInsert(array $data, string $type) { $eventResult = $this->fireEvents('before-insert'); - if (!is_null($eventResult)) { + if (! is_null($eventResult)) { return $eventResult; } // If first value is not an array () not a batch insert) - if (!is_array(current($data))) { + if (! is_array(current($data))) { $queryObject = $this->getQuery($type, $data); list($preparedQuery, $executionTime) = $this->statement($queryObject->getSql(), $queryObject->getBindings()); @@ -584,7 +581,7 @@ private function doInsert(array $data, string $type) $return = 1 === $this->dbInstance->rows_affected ? $this->dbInstance->insert_id : null; } else { // Its a batch insert - $return = []; + $return = array(); $executionTime = 0; foreach ($data as $subData) { $queryObject = $this->getQuery($type, $subData); @@ -644,11 +641,11 @@ public function replace($data) public function update(array $data): ?int { $eventResult = $this->fireEvents('before-update'); - if (!is_null($eventResult)) { + if (! is_null($eventResult)) { return $eventResult; } $queryObject = $this->getQuery('update', $data); - $r = $this->statement($queryObject->getSql(), $queryObject->getBindings()); + $r = $this->statement($queryObject->getSql(), $queryObject->getBindings()); list($preparedQuery, $executionTime) = $r; $this->dbInstance()->get_results($preparedQuery); $this->fireEvents('after-update', $queryObject, $executionTime); @@ -666,7 +663,7 @@ public function update(array $data): ?int * * @return int|int[]|null will return row id(s) for insert and null for success/fail on update */ - public function updateOrInsert(array $attributes, array $values = []) + public function updateOrInsert(array $attributes, array $values = array()) { // Check if existing post exists. $query = clone $this; @@ -697,7 +694,7 @@ public function onDuplicateKeyUpdate($data) public function delete() { $eventResult = $this->fireEvents('before-delete'); - if (!is_null($eventResult)) { + if (! is_null($eventResult)) { return $eventResult; } @@ -719,7 +716,7 @@ public function delete() */ public function table(...$tables) { - $instance = $this->constructCurrentBuilderClass($this->connection); + $instance = $this->constructCurrentBuilderClass($this->connection); $instance->setFetchMode($this->getFetchMode(), $this->hydratorConstructorArgs); foreach ($tables as $table) { @@ -741,6 +738,9 @@ public function table(...$tables) */ public function from(...$tables): self { + foreach ($tables as $table) { + $this->getStatementCollection()->addTable(new TableStatement($table)); + } $tables = $this->addTablePrefix($tables, false); $this->addStatement('tables', $tables); @@ -758,7 +758,7 @@ public function select($fields): self // if (!is_array($fields)) { // $fields = func_get_args(); // } - $this->selectHandler(!is_array($fields) ? func_get_args() : $fields); + $this->selectHandler(! is_array($fields) ? func_get_args() : $fields); return $this; } @@ -772,23 +772,35 @@ public function selectDistinct($fields) // $this->select($fields, true); // $this->addStatement('distinct', true); $this->selectHandler( - !is_array($fields) ? func_get_args() : $fields, + ! is_array($fields) ? func_get_args() : $fields, true ); return $this; } + /** + * Handles an mixed array of selects and creates the statements based on being DISTINCT or not. + * + * @param array $selects + * @param bool $isDistinct + * @return void + */ private function selectHandler(array $selects, bool $isDistinct = false): void { $selects2 = $this->maybeFlipArrayValues($selects); - foreach ($selects2 as ["key" => $field, "value" => $alias]) { + foreach ($selects2 as ['key' => $field, 'value' => $alias]) { // If no alias passed, but field is for JSON. thrown an exception. if (is_numeric($field) && is_string($alias) && $this->jsonHandler->isJsonSelector($alias)) { - throw new Exception("An alias must be used if you wish to select from JSON Object", 1); + throw new Exception('An alias must be used if you wish to select from JSON Object', 1); + } + + if (is_int($field)) { + continue; } + /** V0.2 */ - $statement = is_numeric($alias) + $statement = ! is_string($alias) ? new SelectStatement($field) : new SelectStatement($field, $alias); $this->statementCollection->addSelect( @@ -796,12 +808,10 @@ private function selectHandler(array $selects, bool $isDistinct = false): void ); } - - foreach ($selects as $field => $alias) { // If no alias passed, but field is for JSON. thrown an exception. if (is_numeric($field) && is_string($alias) && $this->jsonHandler->isJsonSelector($alias)) { - throw new Exception("An alias must be used if you wish to select from JSON Object", 1); + throw new Exception('An alias must be used if you wish to select from JSON Object', 1); } // /** V0.2 */ @@ -822,10 +832,9 @@ private function selectHandler(array $selects, bool $isDistinct = false): void $field = $this->addTablePrefix($field); // Treat each array as a single table, to retain order added - $field = is_numeric($field) + $field = is_numeric($field) ? $field = $alias // If single colum - : $field = [$field => $alias]; // Has alias - + : $field = array( $field => $alias ); // Has alias $this->addStatement('selects', $field); /** REMOVE ABOVE IN V0.2 */ @@ -839,7 +848,7 @@ private function selectHandler(array $selects, bool $isDistinct = false): void */ public function groupBy($field): self { - $groupBys = is_array($field) ? $field : [$field]; + $groupBys = is_array($field) ? $field : array( $field ); foreach (array_filter($groupBys, 'is_string') as $groupBy) { $this->statementCollection->addGroupBy(new GroupByStatement($groupBy)); } @@ -870,8 +879,14 @@ public function groupBy($field): self public function _maybeFlipArrayValues($key, $value): array { return is_object($value) || is_int($key) - ? ['key' => $value, 'value' => $key] - : ['key' => $key, 'value' => $value]; + ? array( + 'key' => $value, + 'value' => $key, + ) + : array( + 'key' => $key, + 'value' => $value, + ); } /** @@ -884,17 +899,24 @@ public function _maybeFlipArrayValues($key, $value): array * ['key' => Raw::val('count(foo)'), value => 'aliasB'], * ['key' => 'noAlias', 'value'=> 0 ] * ] - * - * @param array $data - * @return array{key:string|object|int,value:string|object|int}[] + * @template K The key + * @template V The value + * @param array $data + * @return array */ public function maybeFlipArrayValues(array $data): array { return array_map( function ($key, $value): array { return is_object($value) || is_int($key) - ? ['key' => $value, 'value' => $key] - : ['key' => $key, 'value' => $value]; + ? array( + 'key' => $value, + 'value' => $key, + ) + : array( + 'key' => $key, + 'value' => $value, + ); }, array_keys($data), array_values($data) @@ -909,19 +931,24 @@ function ($key, $value): array { */ public function orderBy($fields, string $defaultDirection = 'ASC'): self { - if (!is_array($fields)) { - $fields = [$fields]; + if (! is_array($fields)) { + $fields = array( $fields ); } foreach ( // Key = Column && Value = Direction $this->maybeFlipArrayValues($fields) - as ["key" => $column, "value" => $direction] + as + ['key' => $column, + 'value' => $direction] + ) { - $this->statementCollection->addOrderBy(new OrderByStatement( - $column, // @phpstan-ignore-line - is_int($direction) ? $defaultDirection : (string) $direction - )); + $this->statementCollection->addOrderBy( + new OrderByStatement( + $column, // @phpstan-ignore-line + ! is_string($direction) ? $defaultDirection : (string) $direction + ) + ); } /** REMOVE BELOW HERE IN v0.2 */ @@ -937,7 +964,7 @@ public function orderBy($fields, string $defaultDirection = 'ASC'): self $field = $this->jsonHandler->extractAndUnquoteFromJsonSelector($field); // @phpstan-ignore-line } - if (!$field instanceof Raw) { + if (! $field instanceof Raw) { $field = $this->addTablePrefix($field); // @phpstan-ignore-line } $this->statements['orderBys'][] = compact('field', 'type'); @@ -1136,7 +1163,7 @@ public function orWhereNotIn($key, $values): self */ public function whereBetween($key, $valueFrom, $valueTo): self { - return $this->whereHandler($key, 'BETWEEN', [$valueFrom, $valueTo], 'AND'); + return $this->whereHandler($key, 'BETWEEN', array( $valueFrom, $valueTo ), 'AND'); } /** @@ -1148,7 +1175,7 @@ public function whereBetween($key, $valueFrom, $valueTo): self */ public function orWhereBetween($key, $valueFrom, $valueTo): self { - return $this->whereHandler($key, 'BETWEEN', [$valueFrom, $valueTo], 'OR'); + return $this->whereHandler($key, 'BETWEEN', array( $valueFrom, $valueTo ), 'OR'); } /** @@ -1387,7 +1414,7 @@ public function join($table, $key, ?string $operator = null, $value = null, $typ $value = $this->jsonHandler->extractAndUnquoteFromJsonSelector($value); } - if (!$key instanceof Closure) { + if (! $key instanceof Closure) { $key = function (JoinBuilder $joinBuilder) use ($key, $operator, $value) { $joinBuilder->on($key, $operator, $value); }; @@ -1395,7 +1422,7 @@ public function join($table, $key, ?string $operator = null, $value = null, $typ // Build a new JoinBuilder class, keep it by reference so any changes made // in the closure should reflect here - $joinBuilder = new JoinBuilder($this->connection); + $joinBuilder = new JoinBuilder($this->connection); // Call the closure with our new joinBuilder object $key($joinBuilder); @@ -1483,8 +1510,8 @@ public function outerJoin($table, $key, $operator = null, $value = null) */ public function joinUsing(string $table, string $key, string $type = 'INNER'): self { - if (!array_key_exists('tables', $this->statements) || count($this->statements['tables']) !== 1) { - throw new Exception("JoinUsing can only be used with a single table set as the base of the query", 1); + if (! array_key_exists('tables', $this->statements) || count($this->statements['tables']) !== 1) { + throw new Exception('JoinUsing can only be used with a single table set as the base of the query', 1); } $baseTable = end($this->statements['tables']); @@ -1494,7 +1521,7 @@ public function joinUsing(string $table, string $key, string $type = 'INNER'): s } $remoteKey = $table = $this->addTablePrefix("{$table}.{$key}", true); - $localKey = $table = $this->addTablePrefix("{$baseTable}.{$key}", true); + $localKey = $table = $this->addTablePrefix("{$baseTable}.{$key}", true); return $this->join($table, $remoteKey, '=', $localKey, $type); } @@ -1506,7 +1533,7 @@ public function joinUsing(string $table, string $key, string $type = 'INNER'): s * * @return Raw */ - public function raw($value, $bindings = []): Raw + public function raw($value, $bindings = array()): Raw { return new Raw($value, $bindings); } @@ -1574,14 +1601,14 @@ protected function whereHandler($key, $operator = null, $value = null, $joiner = */ protected function addStatement($key, $value) { - if (!is_array($value)) { - $value = [$value]; + if (! is_array($value)) { + $value = array( $value ); } - if (!array_key_exists($key, $this->statements)) { - $this->statements[$key] = $value; + if (! array_key_exists($key, $this->statements)) { + $this->statements[ $key ] = $value; } else { - $this->statements[$key] = array_merge($this->statements[$key], $value); + $this->statements[ $key ] = array_merge($this->statements[ $key ], $value); } } @@ -1639,7 +1666,7 @@ public function fireEvents(string $event) $params = func_get_args(); // @todo Replace this with an easier to read alteratnive array_unshift($params, $this); - return call_user_func_array([$this->connection->getEventHandler(), 'fireEvents'], $params); + return call_user_func_array(array( $this->connection->getEventHandler(), 'fireEvents' ), $params); } /** diff --git a/src/QueryBuilder/WPDBAdapter.php b/src/QueryBuilder/WPDBAdapter.php index e1c622c..c7f70a7 100644 --- a/src/QueryBuilder/WPDBAdapter.php +++ b/src/QueryBuilder/WPDBAdapter.php @@ -6,17 +6,17 @@ use Pixie\Binding; use Pixie\Exception; -use function is_bool; - use Pixie\Connection; -use function is_float; - use Pixie\QueryBuilder\Raw; + use Pixie\Parser\StatementParser; + use Pixie\Statement\SelectStatement; use Pixie\QueryBuilder\NestedCriteria; use Pixie\Statement\StatementCollection; +use function is_bool; +use function is_float; class WPDBAdapter { From ecaf00c800f6488afd6be7125c6728bc1aae2d95 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Wed, 16 Feb 2022 23:41:59 +0000 Subject: [PATCH 17/24] House Keeping --- src/QueryBuilder/QueryBuilderHandler.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index 0e5c477..7f1dafa 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -943,9 +943,13 @@ public function orderBy($fields, string $defaultDirection = 'ASC'): self 'value' => $direction] ) { + // To please static analysis due to limited generics + if (is_int($column)) { + continue; + } $this->statementCollection->addOrderBy( new OrderByStatement( - $column, // @phpstan-ignore-line + $column, ! is_string($direction) ? $defaultDirection : (string) $direction ) ); From 8c86bb392ceba9e43155cf0786ec6617a604efb8 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Wed, 16 Feb 2022 23:53:11 +0000 Subject: [PATCH 18/24] Reneamed statementCollection to StatementBuilder --- src/QueryBuilder/QueryBuilderHandler.php | 32 +++++++++---------- src/QueryBuilder/WPDBAdapter.php | 4 +-- ...entCollection.php => StatementBuilder.php} | 2 +- tests/Unit/Parser/TestStatementParser.php | 8 ++--- .../Statement/TestStatementCollection.php | 12 +++---- tests/Unit/TestQueryBuilderHandler.php | 2 +- workspace.code-workspace | 3 +- 7 files changed, 31 insertions(+), 32 deletions(-) rename src/Statement/{StatementCollection.php => StatementBuilder.php} (99%) diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index 7f1dafa..6b91c39 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -8,9 +8,7 @@ use Pixie\Binding; use Pixie\Exception; use Pixie\Connection; - use Pixie\HasConnection; - use Pixie\JSON\JsonHandler; use Pixie\QueryBuilder\Raw; use Pixie\JSON\JsonSelector; @@ -24,7 +22,7 @@ use Pixie\QueryBuilder\TablePrefixer; use Pixie\Statement\GroupByStatement; use Pixie\Statement\OrderByStatement; -use Pixie\Statement\StatementCollection; +use Pixie\Statement\StatementBuilder; use function mb_strlen; class QueryBuilderHandler implements HasConnection @@ -44,8 +42,8 @@ class QueryBuilderHandler implements HasConnection */ protected $statements = array(); - /** @var StatementCollection */ - protected $statementCollection; + /** @var StatementBuilder */ + protected $StatementBuilder; /** * @var wpdb @@ -122,7 +120,7 @@ final public function __construct( $this->jsonHandler = new JsonHandler($connection); // Setup statement collection. - $this->statementCollection = new StatementCollection(); + $this->StatementBuilder = new StatementBuilder(); } /** @@ -531,7 +529,7 @@ public function getQuery(string $type = 'select', $dataToBePassed = array()) } if ('select' === $type) { - $queryArr = $this->adapterInstance->selectCol($this->statementCollection, array(), $this->statements); + $queryArr = $this->adapterInstance->selectCol($this->StatementBuilder, array(), $this->statements); } else { $queryArr = $this->adapterInstance->$type($this->statements, $dataToBePassed); } @@ -720,7 +718,7 @@ public function table(...$tables) $instance->setFetchMode($this->getFetchMode(), $this->hydratorConstructorArgs); foreach ($tables as $table) { - $instance->getStatementCollection()->addTable(new TableStatement($table)); + $instance->getStatementBuilder()->addTable(new TableStatement($table)); } /** REMOVE BELOW HERE IN V0.2 */ @@ -739,7 +737,7 @@ public function table(...$tables) public function from(...$tables): self { foreach ($tables as $table) { - $this->getStatementCollection()->addTable(new TableStatement($table)); + $this->getStatementBuilder()->addTable(new TableStatement($table)); } $tables = $this->addTablePrefix($tables, false); $this->addStatement('tables', $tables); @@ -803,7 +801,7 @@ private function selectHandler(array $selects, bool $isDistinct = false): void $statement = ! is_string($alias) ? new SelectStatement($field) : new SelectStatement($field, $alias); - $this->statementCollection->addSelect( + $this->StatementBuilder->addSelect( $statement->setIsDistinct($isDistinct) ); } @@ -818,7 +816,7 @@ private function selectHandler(array $selects, bool $isDistinct = false): void // $statement = is_numeric($field) // ? new SelectStatement($alias) // : new SelectStatement($field, $alias); - // $this->statementCollection->addSelect( + // $this->StatementBuilder->addSelect( // $statement/* ->setIsDistinct($isDistinct) */ // ); @@ -850,7 +848,7 @@ public function groupBy($field): self { $groupBys = is_array($field) ? $field : array( $field ); foreach (array_filter($groupBys, 'is_string') as $groupBy) { - $this->statementCollection->addGroupBy(new GroupByStatement($groupBy)); + $this->StatementBuilder->addGroupBy(new GroupByStatement($groupBy)); } /** REMOVE BELOW IN V0.2 */ @@ -947,7 +945,7 @@ public function orderBy($fields, string $defaultDirection = 'ASC'): self if (is_int($column)) { continue; } - $this->statementCollection->addOrderBy( + $this->StatementBuilder->addOrderBy( new OrderByStatement( $column, ! is_string($direction) ? $defaultDirection : (string) $direction @@ -1702,11 +1700,11 @@ public function jsonBuilder(): JsonQueryBuilder } /** - * Get the value of statementCollection - * @return StatementCollection + * Get the value of StatementBuilder + * @return StatementBuilder */ - public function getStatementCollection(): StatementCollection + public function getStatementBuilder(): StatementBuilder { - return $this->statementCollection; + return $this->StatementBuilder; } } diff --git a/src/QueryBuilder/WPDBAdapter.php b/src/QueryBuilder/WPDBAdapter.php index c7f70a7..9f393d3 100644 --- a/src/QueryBuilder/WPDBAdapter.php +++ b/src/QueryBuilder/WPDBAdapter.php @@ -14,7 +14,7 @@ use Pixie\Statement\SelectStatement; use Pixie\QueryBuilder\NestedCriteria; -use Pixie\Statement\StatementCollection; +use Pixie\Statement\StatementBuilder; use function is_bool; use function is_float; @@ -37,7 +37,7 @@ public function __construct(Connection $connection) } /** This is a mock for the new parser based select method. */ - public function selectCol(StatementCollection $col, $data, $statements) // @phpstan-ignore-line + public function selectCol(StatementBuilder $col, $data, $statements) // @phpstan-ignore-line { if (!array_key_exists('tables', $statements)) { throw new Exception('No table specified.', 3); diff --git a/src/Statement/StatementCollection.php b/src/Statement/StatementBuilder.php similarity index 99% rename from src/Statement/StatementCollection.php rename to src/Statement/StatementBuilder.php index fed41b5..931cd53 100644 --- a/src/Statement/StatementCollection.php +++ b/src/Statement/StatementBuilder.php @@ -28,7 +28,7 @@ use Pixie\Statement\TableStatement; -class StatementCollection +class StatementBuilder { /** * Holds all the statements diff --git a/tests/Unit/Parser/TestStatementParser.php b/tests/Unit/Parser/TestStatementParser.php index 62fbff9..17ec06b 100644 --- a/tests/Unit/Parser/TestStatementParser.php +++ b/tests/Unit/Parser/TestStatementParser.php @@ -23,7 +23,7 @@ use Pixie\Statement\TableStatement; use Pixie\Tests\SQLAssertionsTrait; use Pixie\Statement\SelectStatement; -use Pixie\Statement\StatementCollection; +use Pixie\Statement\StatementBuilder; /** * @group v0.2 @@ -59,7 +59,7 @@ public function getParser(array $connectionConfig = []): StatementParser /** @testdox Is should be possible to parse all expected select values from string, json arrow and selector objects and raw expressions and have them returned as a valid SQL fragment. */ public function testSelectParserWithAcceptTypes(): void { - $collection = new StatementCollection(); + $collection = new StatementBuilder(); // Expected user inputs $collection->addSelect(new SelectStatement('simpleCol')); $collection->addSelect(new SelectStatement('table.simpleCol')); @@ -87,7 +87,7 @@ public function testSelectParserWithAcceptTypes(): void /** @testdox Is should be possible to parse all expected select with aliases values from string, json arrow and selector objects and raw expressions and have them returned as a valid SQL fragment. */ public function testSelectParserWithAcceptTypesWithAliases(): void { - $collection = new StatementCollection(); + $collection = new StatementBuilder(); // Expected user inputs $collection->addSelect(new SelectStatement('simpleCol', 'alias')); $collection->addSelect(new SelectStatement('table.simpleCol', 'alias')); @@ -116,7 +116,7 @@ public function testSelectParserWithAcceptTypesWithAliases(): void public function testTableParserWithoutAliases(): void { // Without prefix - $collection = new StatementCollection(); + $collection = new StatementBuilder(); $collection->addTable(new TableStatement('string')); $collection->addTable(new TableStatement(new Raw('raw(%s)', ['str']))); diff --git a/tests/Unit/Statement/TestStatementCollection.php b/tests/Unit/Statement/TestStatementCollection.php index c894319..fd1cb8c 100644 --- a/tests/Unit/Statement/TestStatementCollection.php +++ b/tests/Unit/Statement/TestStatementCollection.php @@ -3,7 +3,7 @@ declare(strict_types=1); /** - * Unit tests for the StatementCollection + * Unit tests for the StatementBuilder * * @since 0.2.0 * @author GLynn Quelch @@ -15,20 +15,20 @@ use Pixie\Statement\Statement; use Pixie\Statement\TableStatement; use Pixie\Statement\SelectStatement; -use Pixie\Statement\StatementCollection; +use Pixie\Statement\StatementBuilder; /** * @group v0.2 * @group unit * @group statement */ -class TestStatementCollection extends WP_UnitTestCase +class TestStatementBuilder extends WP_UnitTestCase { /** @testdox It should be possible to get the contents of the collection */ public function testGetCollectionItems(): void { - $collection = new StatementCollection(); + $collection = new StatementBuilder(); $array = $collection->getStatements(); // Check all keys exist @@ -48,7 +48,7 @@ public function testGetCollectionItems(): void /** @testdox It should be possible to add, fetch select statements and check if any set. */ public function testSelectStatement(): void { - $collection = new StatementCollection(); + $collection = new StatementBuilder(); // Should be empty $this->assertFalse($collection->hasSelect()); @@ -65,7 +65,7 @@ public function testSelectStatement(): void /** @testdox It should be possible to add, fetch table statements and check if any set. */ public function testTableStatement(): void { - $collection = new StatementCollection(); + $collection = new StatementBuilder(); // Should be empty $this->assertFalse($collection->hasTable()); diff --git a/tests/Unit/TestQueryBuilderHandler.php b/tests/Unit/TestQueryBuilderHandler.php index 08f10b6..2602e61 100644 --- a/tests/Unit/TestQueryBuilderHandler.php +++ b/tests/Unit/TestQueryBuilderHandler.php @@ -305,7 +305,7 @@ public function testOrderByFlipValuesAsOrderBys(): void $builder->orderBy(['ASC' => Raw::val('test')]); $builder->orderBy(['DESC' => new JsonSelector('col', ['nod1', 'nod2'])]); - $statements = $builder->getStatementCollection()->getOrderBy(); + $statements = $builder->getStatementBuilder()->getOrderBy(); $this->assertEquals('column', $statements[0]->getField()); $this->assertEquals('ASC', $statements[0]->getDirection()); diff --git a/workspace.code-workspace b/workspace.code-workspace index ac78f8b..d0f2f09 100644 --- a/workspace.code-workspace +++ b/workspace.code-workspace @@ -9,6 +9,7 @@ "phpcbf.standard": "PSR12", "files.associations": { "*.yaml": "home-assistant" - } + }, + "phpstan.path": "vendor/phpstan/phpstan/phpstan" } } \ No newline at end of file From 585f4c714e813a501d4e13b381b264e0ec2076ad Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Sun, 20 Feb 2022 14:52:54 +0000 Subject: [PATCH 19/24] House Keeping --- src/Criteria/Criteria.php | 75 ++++++ src/Criteria/CriteriaBuilder.php | 270 ++++++++++++++++++++ src/Exception/WpdbException.php | 34 +++ src/Parser/Normalizer.php | 65 +++++ src/Parser/StatementParser.php | 34 +++ src/QueryBuilder/QueryBuilderHandler.php | 82 +++--- src/QueryBuilder/WPDBAdapter.php | 34 ++- src/Statement/HasCriteria.php | 69 +++++ src/Statement/Statement.php | 1 + src/Statement/StatementBuilder.php | 146 +++++++++-- src/Statement/WhereStatement.php | 153 +++++++++++ tests/Unit/Critiera/TestCritieraBuilder.php | 59 +++++ 12 files changed, 947 insertions(+), 75 deletions(-) create mode 100644 src/Criteria/Criteria.php create mode 100644 src/Criteria/CriteriaBuilder.php create mode 100644 src/Exception/WpdbException.php create mode 100644 src/Statement/HasCriteria.php create mode 100644 src/Statement/WhereStatement.php create mode 100644 tests/Unit/Critiera/TestCritieraBuilder.php diff --git a/src/Criteria/Criteria.php b/src/Criteria/Criteria.php new file mode 100644 index 0000000..0c288c6 --- /dev/null +++ b/src/Criteria/Criteria.php @@ -0,0 +1,75 @@ + + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @package Gin0115\Pixie + * @subpackage Criteria + */ + +namespace Pixie\Criteria; + +class Criteria +{ + + /** + * The SQL statement + * + * @var string + */ + protected $statement; + + /** + * The bindings for the statement + * + * @var array + */ + protected $bindings; + + /** + * @param string $statement + * @param array $bindings + */ + public function __construct(string $statement, array $bindings) + { + $this->statement = $statement; + $this->bindings = $bindings; + } + + /** + * Get the SQL statement + * + * @return string + */ + public function getStatement(): string + { + return $this->statement; + } + + /** + * Get the bindings + * + * @returm array + */ + public function getBindings(): array + { + return $this->bindings; + } +} diff --git a/src/Criteria/CriteriaBuilder.php b/src/Criteria/CriteriaBuilder.php new file mode 100644 index 0000000..ef9e5d9 --- /dev/null +++ b/src/Criteria/CriteriaBuilder.php @@ -0,0 +1,270 @@ + + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @package Gin0115\Pixie + * @subpackage Criteria + */ + +namespace Pixie\Criteria; + +use Pixie\Binding; +use Pixie\Connection; +use Pixie\WpdbHandler; +use Pixie\QueryBuilder\Raw; +use Pixie\Parser\Normalizer; +use Pixie\Parser\TablePrefixer; +use Pixie\Statement\HasCriteria; +use Pixie\JSON\JsonSelectorHandler; +use Pixie\JSON\JsonExpressionFactory; + +class CriteriaBuilder +{ + /** BETWEEN TEMPLATE {1: Joiner, 2: Type, 3: Field, 4: Operation, 5: Val1, 6: Val2} */ + protected const TEMPLATE_BETWEEN = "%s%s%s %s %s AND %s"; + + /** IN TEMPLATE {1: Joiner, 2: Type, 3: Field, 4: Operation, 5: Vals (as comma separated array)} */ + protected const TEMPLATE_IN = "%s%s%s %s (%s)"; + + /** + * Hold access to the connection + * + * @var Connection + */ + protected $connection; + + /** + * Holds all composed fragments of the criteria + * + * @var string[] + */ + protected $criteriaFragments = []; + + /** + * Binding values for the criteria. + * + * @var array + */ + protected $bindings = []; + + /** + * Does this criteria use bindings. + * + * @var bool + */ + protected $useBindings; + + /** + * WPDB Access + * + * @var WpdbHandler + */ + protected $wpdbHandler; + + /** @var Normalizer */ + protected $normalizer; + + public function __construct(Connection $connection) + { + $this->connection = $connection; + $this->wpdbHandler = new WpdbHandler($connection); + $this->normalizer = $this->createNormalizer($connection); + } + + /** + * Creates a full populated instance of the normalizer + * + * @param Connection $connection + * @return Normalizer + */ + private function createNormalizer($connection): Normalizer + { + // Create the table prefixer. + $adapterConfig = $connection->getAdapterConfig(); + $prefix = isset($adapterConfig[Connection::PREFIX]) + ? $adapterConfig[Connection::PREFIX] + : null; + + return new Normalizer( + new WpdbHandler($connection), + new TablePrefixer($prefix), + new JsonSelectorHandler(), + new JsonExpressionFactory($connection) + ); + } + + /** + * Returns a new instance of it self + * + * @return self + */ + public function new(): self + { + return new self($this->connection); + } + + /** + * Pushes a set of bindings to the existing collection + * + * @param array $bindings + * @return void + */ + public function pushBindings(array $bindings): void + { + $this->bindings = array_merge($this->bindings, $bindings); + } + + /** + * Pushes a set of criteria fragments to the existing collection + * + * @param array + * @return void + */ + public function pushFragments(array $criteriaFragments): void + { + $this->criteriaFragments = array_merge($this->criteriaFragments, $criteriaFragments); + } + + /** + * Checks if fragments are empty and would be first + * + * @return bool + */ + protected function firstFragment(): bool + { + return 0 === count($this->criteriaFragments); + } + + /** + * Builds the criteria based on an array of statements. + * + * @param HasCriteria[] $statements + * @return self + */ + public function fromStatements(array $statements): self + { + foreach ($statements as $statement) { + $this->processStatement($statement); + } + return $this; + } + + /** + * Return the current criteria + * + * @return Criteria + */ + public function getCriteria(): Criteria + { + return new Criteria( + join(' ', $this->criteriaFragments), + $this->bindings + ); + } + + /** + * Processes a single statement. + * + * @param \Pixie\Statement\HasCriteria $statement + * @return void + */ + protected function processStatement(HasCriteria $statement): void + { + // Based on the statement, build the criteria. + switch (true) { + // NESTED STATEMENT. + case is_a($statement->getField(), \Closure::class) + && null === $statement->getValue(): + $criteria = $this->processNestedQuery($statement); + break; + + // BETWEEN or IN criteria. + case is_array($statement->getValue()): + $criteria = $this->processWithMultipleValues($statement); + break; + + default: + $criteria = new Criteria('MOCK', []); + break; + } + + // Push the current criteria to the collections. + $this->pushBindings($criteria->getBindings()); + $this->pushFragments([$criteria->getStatement()]); + } + + /** + * Processes a nested query + * @param HasCriteria $statement The statement + * @return Criteria + */ + protected function processNestedQuery(HasCriteria $statement): Criteria + { + return new Criteria('MOCK', []); + } + + public function processWithMultipleValues(HasCriteria $statement): Criteria + { + $values = array_map([$this->normalizer, 'normalizeValue'], (array)$statement->getValue()); + + $isBetween = strpos($statement->getOperator(), 'BETWEEN') !== false + && 2 === count($values); + + // Loop through values and build collection of placeholders and bindings + $placeHolder = []; + $bindings = []; + + foreach ($values as $value) { + if ($value instanceof Raw) { + $placeHolder[] = $this->normalizer->parseRaw($value); + } elseif ($value instanceof Binding) { + $placeHolder[] = $value->getType(); + $bindings[] = $value->getValue(); + } + } + + $statement = true === $isBetween + ? sprintf( + self::TEMPLATE_BETWEEN, + $this->firstFragment() ? '' : \strtoupper($statement->getJoiner()) . ' ', + ! $this->firstFragment() ? '' : strtoupper($statement->getCriteriaType()) . ' ', + $this->normalizer->getTablePrefixer()->field($statement->getField()), + strtoupper($statement->getOperator()), + $placeHolder[0], + $placeHolder[1], + ) + : sprintf( + self::TEMPLATE_IN, + $this->firstFragment() ? '' : \strtoupper($statement->getJoiner()) . ' ', + ! $this->firstFragment() ? '' : strtoupper($statement->getCriteriaType()) . ' ', + $this->normalizer->getTablePrefixer()->field($statement->getField()), + strtoupper($statement->getOperator()), + join(', ', $placeHolder), + ); + + return new Criteria( + $statement, + $bindings + ); + + // dump($values); TEMPLATE_IN + } +} diff --git a/src/Exception/WpdbException.php b/src/Exception/WpdbException.php new file mode 100644 index 0000000..171c817 --- /dev/null +++ b/src/Exception/WpdbException.php @@ -0,0 +1,34 @@ + + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @package Gin0115\Pixie + * @subpackage Exception + */ + +namespace Pixie\Exception; + +use Pixie\Exception; + +class WpdbException extends Exception +{ + +} diff --git a/src/Parser/Normalizer.php b/src/Parser/Normalizer.php index 7a24ca1..ef954da 100644 --- a/src/Parser/Normalizer.php +++ b/src/Parser/Normalizer.php @@ -26,6 +26,8 @@ namespace Pixie\Parser; +use Pixie\Binding; +use Pixie\Exception; use Pixie\WpdbHandler; use Pixie\QueryBuilder\Raw; use Pixie\JSON\JsonSelector; @@ -203,4 +205,67 @@ private function normalizeJsonSelector(JsonSelector $selector, bool $isField = t return $this->jsonExpressions->extractAndUnquote($column, $selector->getNodes()) ->getValue(); } + + /** + * Normalizes a values to either a bindings or raw statement. + * + * @param Raw|Binding|string|float|int|bool|null $value + * @return void + */ + public function normalizeValue($value) + { + switch (true) { + case $value instanceof Binding && Binding::RAW === $value->getType(): + /** @var Raw */ + $value = $value->getValue(); + break; + + case is_string($value): + $value = Binding::asString($value); + break; + + case is_int($value): + case is_bool($value): + $value = Binding::asInt($value); + break; + + case is_float($value): + $value = Binding::asFloat($value); + break; + + case $value instanceof Binding || $value instanceof Raw: + $value = $value; + break; + + default: + // dump($value); + throw new Exception(\sprintf("Unexpected type :: %s", print_r($value, true)), 1); + } + + return $value; + } + + /** + * Attempts to parse a raw query, if bindings are defined then they will be bound first. + * + * @param Raw $raw + * @requires string + */ + public function parseRaw(Raw $raw): string + { + $bindings = $raw->getBindings(); + return 0 === count($bindings) + ? (string) $raw + : $this->wpdbHandler->interpolateQuery($raw->getValue(), $bindings); + } + + /** + * Get access to the table prefixer. + * + * @return TablePrefixer + */ + public function getTablePrefixer(): TablePrefixer + { + return $this->tablePrefixer; + } } diff --git a/src/Parser/StatementParser.php b/src/Parser/StatementParser.php index 17bfb09..24ccd31 100644 --- a/src/Parser/StatementParser.php +++ b/src/Parser/StatementParser.php @@ -28,6 +28,7 @@ use Pixie\Connection; use Pixie\WpdbHandler; +use Pixie\Parser\Normalizer; use Pixie\JSON\JsonSelectorHandler; use Pixie\Statement\TableStatement; use Pixie\Statement\SelectStatement; @@ -40,6 +41,8 @@ class StatementParser protected const TEMPLATE_AS = "%s AS %s"; protected const TEMPLATE_ORDER_BY = "ORDER BY %s"; protected const TEMPLATE_GROUP_BY = "GROUP BY %s"; + protected const TEMPLATE_LIMIT = "LIMIT %d"; + protected const TEMPLATE_OFFSET = "OFFSET %d"; /** * @var Connection @@ -171,4 +174,35 @@ public function parseGroupBy(array $orderBy): string ? '' : sprintf(self::TEMPLATE_GROUP_BY, join(', ', $orderBy)); } + + /** + * Parses a limit statement based on the passed value not being null. + * + * @param int|null $limit + * @return string + */ + public function parseLimit(?int $limit): string + { + return is_int($limit) + ? \sprintf(self::TEMPLATE_LIMIT, $limit) + : ''; + } + + /** + * Parses a offset statement based on the passed value not being null. + * + * @param int|null $offset + * @return string + */ + public function parseOffset(?int $offset): string + { + return is_int($offset) + ? \sprintf(self::TEMPLATE_OFFSET, $offset) + : ''; + } + + public function parseWhere(array $where): string + { + # code... + } } diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index 6b91c39..fe83716 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -8,6 +8,7 @@ use Pixie\Binding; use Pixie\Exception; use Pixie\Connection; +use function mb_strlen; use Pixie\HasConnection; use Pixie\JSON\JsonHandler; use Pixie\QueryBuilder\Raw; @@ -18,12 +19,12 @@ use Pixie\QueryBuilder\Transaction; use Pixie\QueryBuilder\WPDBAdapter; use Pixie\Statement\TableStatement; +use Pixie\Statement\WhereStatement; use Pixie\Statement\SelectStatement; use Pixie\QueryBuilder\TablePrefixer; use Pixie\Statement\GroupByStatement; use Pixie\Statement\OrderByStatement; use Pixie\Statement\StatementBuilder; -use function mb_strlen; class QueryBuilderHandler implements HasConnection { @@ -737,7 +738,7 @@ public function table(...$tables) public function from(...$tables): self { foreach ($tables as $table) { - $this->getStatementBuilder()->addTable(new TableStatement($table)); + $this->StatementBuilder->addTable(new TableStatement($table)); } $tables = $this->addTablePrefix($tables, false); $this->addStatement('tables', $tables); @@ -753,40 +754,11 @@ public function from(...$tables): self */ public function select($fields): self { - // if (!is_array($fields)) { - // $fields = func_get_args(); - // } - $this->selectHandler(! is_array($fields) ? func_get_args() : $fields); - return $this; - } - - /** - * @param string|string[]|Raw[]|array $fields - * - * @return static - */ - public function selectDistinct($fields) - { - // $this->select($fields, true); - // $this->addStatement('distinct', true); - $this->selectHandler( - ! is_array($fields) ? func_get_args() : $fields, - true - ); - return $this; - } - - /** - * Handles an mixed array of selects and creates the statements based on being DISTINCT or not. - * - * @param array $selects - * @param bool $isDistinct - * @return void - */ - private function selectHandler(array $selects, bool $isDistinct = false): void - { - $selects2 = $this->maybeFlipArrayValues($selects); - foreach ($selects2 as ['key' => $field, 'value' => $alias]) { + if (!is_array($fields)) { + $fields = func_get_args(); + } + $fields2 = $this->maybeFlipArrayValues($fields); + foreach ($fields2 as ['key' => $field, 'value' => $alias]) { // If no alias passed, but field is for JSON. thrown an exception. if (is_numeric($field) && is_string($alias) && $this->jsonHandler->isJsonSelector($alias)) { throw new Exception('An alias must be used if you wish to select from JSON Object', 1); @@ -796,17 +768,14 @@ private function selectHandler(array $selects, bool $isDistinct = false): void continue; } - /** V0.2 */ $statement = ! is_string($alias) ? new SelectStatement($field) : new SelectStatement($field, $alias); - $this->StatementBuilder->addSelect( - $statement->setIsDistinct($isDistinct) - ); + $this->StatementBuilder->addSelect($statement); } - foreach ($selects as $field => $alias) { + foreach ($fields as $field => $alias) { // If no alias passed, but field is for JSON. thrown an exception. if (is_numeric($field) && is_string($alias) && $this->jsonHandler->isJsonSelector($alias)) { throw new Exception('An alias must be used if you wish to select from JSON Object', 1); @@ -825,7 +794,7 @@ private function selectHandler(array $selects, bool $isDistinct = false): void if ($this->jsonHandler->isJsonSelector($field)) { /** @var string $field */ - $field = $this->jsonHandler->extractAndUnquoteFromJsonSelector($field); // @phpstan-ignore-line + $field = $this->jsonHandler->extractAndUnquoteFromJsonSelector($field); } $field = $this->addTablePrefix($field); @@ -837,6 +806,21 @@ private function selectHandler(array $selects, bool $isDistinct = false): void $this->addStatement('selects', $field); /** REMOVE ABOVE IN V0.2 */ } + return $this; + } + + /** + * @param string|string[]|Raw[]|array $fields + * + * @return static + */ + public function selectDistinct($fields) + { + // $this->select($fields, true); + // $this->addStatement('distinct', true); + $this->StatementBuilder->setDistinctSelect(true); + $this->select(!is_array($fields) ? func_get_args() : $fields); + return $this; } /** @@ -996,7 +980,7 @@ public function orderByJson($key, $jsonKey, string $defaultDirection = 'ASC'): s public function limit(int $limit): self { $this->statements['limit'] = $limit; - + $this->StatementBuilder->setLimit($limit); return $this; } @@ -1008,7 +992,7 @@ public function limit(int $limit): self public function offset(int $offset): self { $this->statements['offset'] = $offset; - + $this->StatementBuilder->setOffset($offset); return $this; } @@ -1055,6 +1039,8 @@ public function where($key, $operator = null, $value = null): self $operator = '='; } + $this->StatementBuilder->addWhere(new WhereStatement($key, $operator, $value, 'AND')); + return $this->whereHandler($key, $operator, $value); } @@ -1073,6 +1059,7 @@ public function orWhere($key, $operator = null, $value = null): self $operator = '='; } + $this->StatementBuilder->addWhere(new WhereStatement($key, $operator, $value, 'OR')); return $this->whereHandler($key, $operator, $value, 'OR'); } @@ -1091,6 +1078,7 @@ public function whereNot($key, $operator = null, $value = null): self $operator = '='; } + $this->StatementBuilder->addWhere(new WhereStatement($key, $operator, $value, 'AND NOT')); return $this->whereHandler($key, $operator, $value, 'AND NOT'); } @@ -1109,6 +1097,7 @@ public function orWhereNot($key, $operator = null, $value = null) $operator = '='; } + $this->StatementBuilder->addWhere(new WhereStatement($key, $operator, $value, 'OR NOT')); return $this->whereHandler($key, $operator, $value, 'OR NOT'); } @@ -1120,6 +1109,7 @@ public function orWhereNot($key, $operator = null, $value = null) */ public function whereIn($key, $values): self { + $this->StatementBuilder->addWhere(new WhereStatement($key, 'IN', $values, 'AND')); return $this->whereHandler($key, 'IN', $values, 'AND'); } @@ -1131,6 +1121,7 @@ public function whereIn($key, $values): self */ public function whereNotIn($key, $values): self { + $this->StatementBuilder->addWhere(new WhereStatement($key, 'NOT IN', $values, 'AND')); return $this->whereHandler($key, 'NOT IN', $values, 'AND'); } @@ -1142,6 +1133,7 @@ public function whereNotIn($key, $values): self */ public function orWhereIn($key, $values): self { + $this->StatementBuilder->addWhere(new WhereStatement($key, 'IN', $values, 'OR')); return $this->whereHandler($key, 'IN', $values, 'OR'); } @@ -1153,6 +1145,7 @@ public function orWhereIn($key, $values): self */ public function orWhereNotIn($key, $values): self { + $this->StatementBuilder->addWhere(new WhereStatement($key, 'NOT IN', $values, 'OR')); return $this->whereHandler($key, 'NOT IN', $values, 'OR'); } @@ -1590,6 +1583,7 @@ protected function whereHandler($key, $operator = null, $value = null, $joiner = } $this->statements['wheres'][] = compact('key', 'operator', 'value', 'joiner'); + // dump($this->statements['wheres']); return $this; } diff --git a/src/QueryBuilder/WPDBAdapter.php b/src/QueryBuilder/WPDBAdapter.php index 9f393d3..b465a2b 100644 --- a/src/QueryBuilder/WPDBAdapter.php +++ b/src/QueryBuilder/WPDBAdapter.php @@ -6,17 +6,18 @@ use Pixie\Binding; use Pixie\Exception; +use function is_bool; + use Pixie\Connection; -use Pixie\QueryBuilder\Raw; +use function is_float; +use Pixie\QueryBuilder\Raw; use Pixie\Parser\StatementParser; - +use Pixie\Criteria\CriteriaBuilder; use Pixie\Statement\SelectStatement; -use Pixie\QueryBuilder\NestedCriteria; use Pixie\Statement\StatementBuilder; -use function is_bool; -use function is_float; +use Pixie\QueryBuilder\NestedCriteria; class WPDBAdapter { @@ -59,7 +60,12 @@ public function selectCol(StatementBuilder $col, $data, $statements) // @phpstan // $selects = $this->arrayStr($statements['selects'], ', '); // Wheres + $criteriaWhere = new CriteriaBuilder($this->connection); + $criteriaWhere->fromStatements($col->getWhere()); + list($whereCriteria, $whereBindings) = $this->buildCriteriaWithType($statements, 'wheres', 'WHERE'); + dump([$criteriaWhere->getCriteria()->getStatement(),$whereCriteria]); + dump([$criteriaWhere->getCriteria()->getBindings(), $whereBindings]); // Group bys // $groupBys = ''; @@ -69,18 +75,18 @@ public function selectCol(StatementBuilder $col, $data, $statements) // @phpstan // Limit and offset - $limit = isset($statements['limit']) ? 'LIMIT ' . (int) $statements['limit'] : ''; - $offset = isset($statements['offset']) ? 'OFFSET ' . (int) $statements['offset'] : ''; + // $limit = isset($statements['limit']) ? 'LIMIT ' . (int) $statements['limit'] : ''; + // $offset = isset($statements['offset']) ? 'OFFSET ' . (int) $statements['offset'] : ''; // Having list($havingCriteria, $havingBindings) = $this->buildCriteriaWithType($statements, 'havings', 'HAVING'); // Joins $joinString = $this->buildJoin($statements); - +// dump($col->getWhere()); /** @var string[] */ $sqlArray = [ - 'SELECT' . ($col->hasDistinctSelect() ? ' DISTINCT' : ''), + 'SELECT' . ($col->getDistinctSelect() ? ' DISTINCT' : ''), $parser->parseSelect($col->getSelect()), 'FROM', $parser->parseTable($col->getTable()), @@ -89,8 +95,8 @@ public function selectCol(StatementBuilder $col, $data, $statements) // @phpstan $parser->parseGroupBy($col->getGroupBy()), $havingCriteria, $parser->parseOrderBy($col->getOrderBy()), - $limit, - $offset, + $parser->parseLimit($col->getLimit()), + $parser->parseOffset($col->getOffset()), ]; $sql = $this->concatenateQuery($sqlArray); @@ -452,7 +458,7 @@ public function delete($statements) list($whereCriteria, $whereBindings) = $this->buildCriteriaWithType($statements, 'wheres', 'WHERE'); // Limit - $limit = isset($statements['limit']) ? 'LIMIT ' . $statements['limit'] : ''; + // $limit = isset($statements['limit']) ? 'LIMIT ' . $statements['limit'] : ''; $sqlArray = ['DELETE FROM', $table, $whereCriteria]; $sql = $this->concatenateQuery($sqlArray); @@ -593,6 +599,9 @@ protected function buildCriteria(array $statements, bool $bindValues = true): ar $queryObject = $nestedCriteria->getQuery('criteriaOnly', true); // Merge the bindings we get from nestedCriteria object $bindings = array_merge($bindings, $queryObject->getBindings()); + + // dump($statement['joiner'], $statement); + // Append the sql we get from the nestedCriteria object $criteria .= $statement['joiner'] . ' (' . $queryObject->getSql() . ') '; } elseif (is_array($value)) { @@ -646,6 +655,7 @@ protected function buildCriteria(array $statements, bool $bindValues = true): ar } else { // Usual where like criteria if (!$bindValues) { + dump(7878789798798798798987); // Specially for joins // We are not binding values, lets sanitize then $value = $this->stringifyValue($this->wrapSanitizer($value)) ?? ''; diff --git a/src/Statement/HasCriteria.php b/src/Statement/HasCriteria.php new file mode 100644 index 0000000..c388752 --- /dev/null +++ b/src/Statement/HasCriteria.php @@ -0,0 +1,69 @@ + + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @package Gin0115\Pixie + * @subpackage QueryBuilder\Statement + */ + +namespace Pixie\Statement; + +interface HasCriteria +{ + + public const WHERE_CRITERIA = 'WHERE'; + public const JOIN_CRITERIA = 'JOIN'; + + /** + * Returns the type of criteria (JOIN, WHERE, HAVING) + * + * @return string + */ + public function getCriteriaType(): string; + + /** + * Gets the field. + * + * @return string|\Closure(QueryBuilderHandler $query):void|Raw|JsonSelector + */ + public function getField(); + + /** + * Get the operator + * + * @return string + */ + public function getOperator(): string; + + /** + * Get value for expression + * + * @return string|int|float|bool|string[]|int[]|float[]|bool[]|null + */ + public function getValue(); + + /** + * Get joiner + * + * @return string + */ + public function getJoiner(): string; +} diff --git a/src/Statement/Statement.php b/src/Statement/Statement.php index 1dc733c..4e848c7 100644 --- a/src/Statement/Statement.php +++ b/src/Statement/Statement.php @@ -35,6 +35,7 @@ interface Statement public const TABLE = 'table'; public const ORDER_BY = 'orderby'; public const GROUP_BY = 'groupby'; + public const WHERE = 'where'; /** * Get the statement type diff --git a/src/Statement/StatementBuilder.php b/src/Statement/StatementBuilder.php index 931cd53..b99b136 100644 --- a/src/Statement/StatementBuilder.php +++ b/src/Statement/StatementBuilder.php @@ -37,7 +37,8 @@ class StatementBuilder * select: SelectStatement[], * table: TableStatement[], * orderby: OrderByStatement[], - * groupby: GroupByStatement[] + * groupby: GroupByStatement[], + * where: WhereStatement[], * } */ protected $statements = [ @@ -45,12 +46,36 @@ class StatementBuilder Statement::TABLE => [], Statement::ORDER_BY => [], Statement::GROUP_BY => [], + Statement::WHERE => [], ]; + /** + * Denotes if a DISTINCT SELECT + * + * @var bool + */ + protected $distinctSelect = false; + + /** + * @var int|null + */ + protected $limit = null; + + /** + * @var int|null + */ + protected $offset = null; + /** * Get all the statements * - * @return array{select:SelectStatement[],table:TableStatement[]} + * @return array{ + * select: SelectStatement[], + * table: TableStatement[], + * orderby: OrderByStatement[], + * groupby: GroupByStatement[], + * where: WhereStatement[], + *} */ public function getStatements(): array { @@ -89,23 +114,6 @@ public function hasSelect(): bool return 0 < count($this->getSelect()); } - /** - * Check if any defined select queries are distinct. - * - * @return bool - */ - public function hasDistinctSelect(): bool - { - $distinctSelects = array_filter( - $this->getSelect(), - function (SelectStatement $select): bool { - return $select->getIsDistinct(); - } - ); - - return 0 < count($distinctSelects); - } - /** * Adds a table statement to the collection. * @@ -201,4 +209,104 @@ public function hasGroupBy(): bool { return 0 < count($this->getGroupBy()); } + + /** + * Adds a select statement to the collection. + * + * @param WhereStatement $statement + * @return self + */ + public function addWhere(WhereStatement $statement): self + { + $this->statements[Statement::WHERE][] = $statement; + return $this; + } + + /** + * Get all WhereStatements + * + * @return WhereStatement[] + */ + public function getWhere(): array + { + return $this->statements[Statement::WHERE]; + } + + /** + * Where statements exist. + * + * @return bool + */ + public function hasWhere(): bool + { + return 0 < count($this->getWhere()); + } + + /** + * Set denotes if a DISTINCT SELECT + * + * @param bool $distinctSelect Denotes if a DISTINCT SELECT + * + * @return static + */ + public function setDistinctSelect(bool $distinctSelect): self + { + $this->distinctSelect = $distinctSelect; + + return $this; + } + + /** + * Get denotes if a DISTINCT SELECT + * + * @return bool + */ + public function getDistinctSelect(): bool + { + return $this->distinctSelect; + } + + /** + * Get the value of limit + * + * @return int|null + */ + public function getLimit(): ?int + { + return $this->limit; + } + + /** + * Set the value of limit + * + * @param int|null $limit + * @return static + */ + public function setLimit(?int $limit): self + { + $this->limit = $limit; + return $this; + } + + /** + * Get the value of offset + * + * @return int|null + */ + public function getOffset(): ?int + { + return $this->offset; + } + + /** + * Set the value of offset + * + * @param int|null $offset + * @return static + */ + public function setOffset(?int $offset): self + { + $this->offset = $offset; + return $this; + } } diff --git a/src/Statement/WhereStatement.php b/src/Statement/WhereStatement.php new file mode 100644 index 0000000..e9b6a49 --- /dev/null +++ b/src/Statement/WhereStatement.php @@ -0,0 +1,153 @@ + + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @package Gin0115\Pixie + * @subpackage QueryBuilder\Statement + */ + +namespace Pixie\Statement; + +use Closure; +use TypeError; +use Pixie\QueryBuilder\Raw; +use Pixie\JSON\JsonSelector; +use Pixie\Statement\Statement; +use Pixie\Statement\HasCriteria; +use Pixie\QueryBuilder\QueryBuilderHandler; + +class WhereStatement implements Statement, HasCriteria +{ + /** + * The field which is being group by + * + * @var string|\Closure(QueryBuilderHandler $query):void|Raw + */ + protected $field; + + /** + * The operator + * + * @var string + */ + protected $operator; + + /** + * Value for expression + * + * @var string|int|float|bool|string[]|int[]|float[]|bool[]|null + */ + protected $value; + + /** + * Joiner + * + * @var string + */ + protected $joiner; + + /** + * Creates a Select Statement + * + * @param string|\Closure(QueryBuilderHandler $query):void|Raw|JsonSelector $field + * @param string $operator + * @param string|int|float|bool|string[]|int[]|float[]|bool[] $value + * @param string $joiner + */ + public function __construct($field, $operator = null, $value = null, string $joiner = 'AND') + { + // Verify valid field type. + $this->verifyField($field); + $this->field = $field; + $this->operator = $operator ?? '='; + $this->value = $value; + $this->joiner = $joiner; + } + + /** @inheritDoc */ + public function getCriteriaType(): string + { + return HasCriteria::WHERE_CRITERIA; + } + + /** @inheritDoc */ + public function getType(): string + { + return Statement::WHERE; + } + + /** + * Verifies if the passed filed is of a valid type. + * + * @param mixed $field + * @return void + */ + protected function verifyField($field): void + { + if ( + !is_string($field) + && ! is_a($field, Raw::class) + && !is_a($field, JsonSelector::class) + && !is_a($field, \Closure::class) + ) { + throw new TypeError("Only strings, Raw, JsonSelector and Closures may be used as fields in Where statements."); + } + } + /** + * Gets the field. + * + * @return string|\Closure(QueryBuilderHandler $query):void|Raw|JsonSelector + */ + public function getField() + { + return $this->field; + } + + /** + * Get the operator + * + * @return string + */ + public function getOperator(): string + { + return $this->operator; + } + + /** + * Get value for expression + * + * @return string|int|float|bool|string[]|int[]|float[]|bool[]|null + */ + public function getValue() + { + return $this->value; + } + + /** + * Get joiner + * + * @return string + */ + public function getJoiner(): string + { + return $this->joiner; + } +} diff --git a/tests/Unit/Critiera/TestCritieraBuilder.php b/tests/Unit/Critiera/TestCritieraBuilder.php new file mode 100644 index 0000000..257f1cf --- /dev/null +++ b/tests/Unit/Critiera/TestCritieraBuilder.php @@ -0,0 +1,59 @@ + + */ + +namespace Pixie\Tests\Unit\Criteria; + +use Pixie\Binding; +use WP_UnitTestCase; +use Pixie\Connection; +use Pixie\QueryBuilder\Raw; +use Pixie\Tests\Logable_WPDB; +use Pixie\Criteria\CriteriaBuilder; +use Pixie\Statement\WhereStatement; + +/** + * @group v0.2 + * @group unit + * @group criteria + */ +class TestCritieraBuilder extends WP_UnitTestCase +{ + /** Mocked WPDB instance. + * @var Logable_WPDB + */ + private $wpdb; + + public function setUp(): void + { + $this->wpdb = new Logable_WPDB(); + parent::setUp(); + } + + /** + * Create an instance of the CriteriaBuilder with a defined connection config. + * + * @param array $connectionConfig + * @return \CriteriaBuilder + */ + public function getBuilder(array $connectionConfig = []): CriteriaBuilder + { + return new CriteriaBuilder(new Connection($this->wpdb, $connectionConfig)); + } + + public function testBuildWhereBetween(): void + { + $statement = new WhereStatement('table.field', 'NOT BETWEEN', [Binding::asInt('2'), new Raw(12)]); + $statement1 = new WhereStatement('table.field2', 'BETWEEN', [Binding::asInt('123'), 787879], 'OR'); + $builder = $this->getBuilder(['prefix' => 'ff_']); + $builder->fromStatements([$statement,$statement1]); + // dump($builder->getCriteria()); + } +} From 2b00c8b4f8414f9be62f2b20d24b4da6321cf461 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Wed, 23 Feb 2022 23:46:34 +0000 Subject: [PATCH 20/24] Where criteria builder now in place --- src/Criteria/Criteria.php | 3 +- src/Criteria/CriteriaBuilder.php | 232 ++++++++++++++++-- src/Exception/WpdbException.php | 1 - src/Parser/Normalizer.php | 95 ++++++- src/Parser/StatementParser.php | 19 +- src/QueryBuilder/JsonQueryBuilder.php | 14 +- src/QueryBuilder/QueryBuilderHandler.php | 54 ++-- src/QueryBuilder/WPDBAdapter.php | 27 +- src/Statement/HasCriteria.php | 7 +- src/Statement/StatementBuilder.php | 2 +- src/Statement/WhereStatement.php | 4 +- tests/Logable_WPDB.php | 2 + .../TestIntegrationWithWPDB.php | 12 +- .../TestQueryBuilderSQLGeneration.php | 4 +- .../TestQueryBuilderWPDBPrepare.php | 6 +- tests/Unit/TestBinding.php | 5 +- 16 files changed, 407 insertions(+), 80 deletions(-) diff --git a/src/Criteria/Criteria.php b/src/Criteria/Criteria.php index 0c288c6..7687f54 100644 --- a/src/Criteria/Criteria.php +++ b/src/Criteria/Criteria.php @@ -28,7 +28,6 @@ class Criteria { - /** * The SQL statement * @@ -66,7 +65,7 @@ public function getStatement(): string /** * Get the bindings * - * @returm array + * @return array */ public function getBindings(): array { diff --git a/src/Criteria/CriteriaBuilder.php b/src/Criteria/CriteriaBuilder.php index ef9e5d9..828c38f 100644 --- a/src/Criteria/CriteriaBuilder.php +++ b/src/Criteria/CriteriaBuilder.php @@ -27,14 +27,17 @@ namespace Pixie\Criteria; use Pixie\Binding; +use Pixie\Exception; use Pixie\Connection; use Pixie\WpdbHandler; use Pixie\QueryBuilder\Raw; +use Pixie\JSON\JsonSelector; use Pixie\Parser\Normalizer; use Pixie\Parser\TablePrefixer; use Pixie\Statement\HasCriteria; use Pixie\JSON\JsonSelectorHandler; use Pixie\JSON\JsonExpressionFactory; +use Pixie\QueryBuilder\NestedCriteria; class CriteriaBuilder { @@ -44,6 +47,15 @@ class CriteriaBuilder /** IN TEMPLATE {1: Joiner, 2: Type, 3: Field, 4: Operation, 5: Vals (as comma separated array)} */ protected const TEMPLATE_IN = "%s%s%s %s (%s)"; + /** SIMPLE TEMPLATE {1: Joiner, 2: Type, 3: Field, 4: Operation, 5: Val} */ + protected const TEMPLATE_SIMPLE = "%s%s%s %s %s"; + + /** EXPRESSION TEMPLATE {1: Joiner, 2: Type, 3: Expression} */ + protected const TEMPLATE_EXPRESSION = "%s%s %s"; + + /** NESTED TEMPLATE {1: Joiner, 2: Type, 3: Expression} */ + protected const TEMPLATE_NESTED = "%s%s(%s)"; + /** * Hold access to the connection * @@ -70,7 +82,7 @@ class CriteriaBuilder * * @var bool */ - protected $useBindings; + protected $useBindings = true; /** * WPDB Access @@ -135,7 +147,7 @@ public function pushBindings(array $bindings): void /** * Pushes a set of criteria fragments to the existing collection * - * @param array + * @param string[] $criteriaFragments * @return void */ public function pushFragments(array $criteriaFragments): void @@ -143,6 +155,18 @@ public function pushFragments(array $criteriaFragments): void $this->criteriaFragments = array_merge($this->criteriaFragments, $criteriaFragments); } + /** + * Set does this criteria use bindings. + * + * @param bool $useBindings Does this criteria use bindings. + * @return self + */ + public function useBindings(bool $useBindings = true) + { + $this->useBindings = $useBindings; + return $this; + } + /** * Checks if fragments are empty and would be first * @@ -176,10 +200,28 @@ public function getCriteria(): Criteria { return new Criteria( join(' ', $this->criteriaFragments), - $this->bindings + array_filter($this->bindings, function ($binding): bool { + return false === is_null($binding); + }) ); } + /** + * Parses a simple (string or Raw) field + * + * @param Raw|\Closure|JsonSelector|string|null $field + * @return string + * @throws Exception If none string or Raw passed. + */ + public function parseBasicField($field): string + { + /** @phpstan-var string|Raw|\Closure $field */ + $field = $this->normalizer->normalizeField($field); + $field = $this->normalizer->normalizeForSQL($field); + /** @phpstan-var string $field */ + return $this->normalizer->getTablePrefixer()->field($field); + } + /** * Processes a single statement. * @@ -201,13 +243,35 @@ protected function processStatement(HasCriteria $statement): void $criteria = $this->processWithMultipleValues($statement); break; + // Where field is raw and value is null (whereNull) + case is_a($statement->getField(), Raw::class) + && null === $statement->getValue(): + $criteria = $this->processRawExpression($statement); + break; + + case is_a($statement->getField(), Raw::class): + case is_a($statement->getField(), JsonSelector::class): + $criteria = $this->processObjectField($statement); + break; + + case is_object($statement->getValue()) + && is_a($statement->getValue(), Raw::class): + $criteria = $this->processSimpleCriteria($statement); + break; + + + + default: - $criteria = new Criteria('MOCK', []); + // $criteria = new Criteria('MOCK', []); + $criteria = $this->processSimpleCriteria($statement); break; } - // Push the current criteria to the collections. - $this->pushBindings($criteria->getBindings()); + // Push bindings, unless specified not to. + if (true === $this->useBindings) { + $this->pushBindings($criteria->getBindings()); + } $this->pushFragments([$criteria->getStatement()]); } @@ -218,15 +282,44 @@ protected function processStatement(HasCriteria $statement): void */ protected function processNestedQuery(HasCriteria $statement): Criteria { - return new Criteria('MOCK', []); + // Ensure only raw can be used as Field. + if (! $statement->getField() instanceof \Closure) { + throw new Exception(sprintf("Nested queries can only be used with a closure as the field., %s passed", json_encode($statement)), 1); + } + + $nestedCriteria = new NestedCriteria($this->connection); + + // Call the closure with our new nestedCriteria object + $statement->getField()($nestedCriteria); + $queryObject = $nestedCriteria->getQuery('criteriaOnly', true); + + $sql = \sprintf( + self::TEMPLATE_NESTED, + $this->firstFragment() ? '' : \strtoupper($statement->getJoiner()) . ' ', + ! $this->firstFragment() ? '' : strtoupper($statement->getCriteriaType()), + $queryObject->getSql() + ); + + return new Criteria( + $sql, + $queryObject->getBindings() + ); } - public function processWithMultipleValues(HasCriteria $statement): Criteria + /** + * Process criteria with an array of values + * @param HasCriteria $statement Where or Having statement + * @return Criteria + */ + protected function processWithMultipleValues(HasCriteria $statement): Criteria { - $values = array_map([$this->normalizer, 'normalizeValue'], (array)$statement->getValue()); - - $isBetween = strpos($statement->getOperator(), 'BETWEEN') !== false - && 2 === count($values); + $values = array_map( + [$this->normalizer, 'normalizeValue'], + is_array($statement->getValue()) + ? $statement->getValue() + : [$statement->getValue() + ] + ); // Loop through values and build collection of placeholders and bindings $placeHolder = []; @@ -236,17 +329,26 @@ public function processWithMultipleValues(HasCriteria $statement): Criteria if ($value instanceof Raw) { $placeHolder[] = $this->normalizer->parseRaw($value); } elseif ($value instanceof Binding) { - $placeHolder[] = $value->getType(); + // Set as placeholder if we are using bindings + $placeHolder[] = true === $this->useBindings + ? $value->getType() + : $value->getValue(); $bindings[] = $value->getValue(); } } - $statement = true === $isBetween + // Parse any Raw in Bindings. + $bindings = array_map($this->normalizer->parseRawCallback(), $bindings); + + // If we have a valid BETWEEN statement, + // use TEMPLATE_BETWEEN else TEMPLATE_IN + $statement = strpos($statement->getOperator(), 'BETWEEN') !== false + && 2 === count($values) ? sprintf( self::TEMPLATE_BETWEEN, $this->firstFragment() ? '' : \strtoupper($statement->getJoiner()) . ' ', ! $this->firstFragment() ? '' : strtoupper($statement->getCriteriaType()) . ' ', - $this->normalizer->getTablePrefixer()->field($statement->getField()), + $this->parseBasicField($statement->getField()), strtoupper($statement->getOperator()), $placeHolder[0], $placeHolder[1], @@ -255,16 +357,108 @@ public function processWithMultipleValues(HasCriteria $statement): Criteria self::TEMPLATE_IN, $this->firstFragment() ? '' : \strtoupper($statement->getJoiner()) . ' ', ! $this->firstFragment() ? '' : strtoupper($statement->getCriteriaType()) . ' ', - $this->normalizer->getTablePrefixer()->field($statement->getField()), + $this->parseBasicField($statement->getField()), strtoupper($statement->getOperator()), join(', ', $placeHolder), ); + return new Criteria($statement, $bindings); + } + + /** + * Process a simple statement + * @param HasCriteria $statement + * @return Criteria + * @throws Exception + */ + protected function processSimpleCriteria(HasCriteria $statement): Criteria + { + // Only allow single values to be processed as simple. + $value = $statement->getValue(); + if (is_array($value)) { + throw new Exception(sprintf("Simple criteria must only have a single value, %s passed", json_encode($value)), 1); + } + $value = $this->normalizer->normalizeValue($value); + + // Set the placeholder and binding based on type + if ($value instanceof Raw) { + $placeHolder = $this->normalizer->parseRaw($value); + $bindings = []; + } else { + // Set as placeholder if we are using bindings + $placeHolder = true === $this->useBindings + ? $value->getType() + : $value->getValue(); + $bindings = [$value->getValue()]; + } + + $sql = sprintf( + self::TEMPLATE_SIMPLE, + ! $this->firstFragment() ? '' : strtoupper($statement->getCriteriaType()) . ' ', + \strtoupper($statement->getJoiner()) . ' ', + $this->parseBasicField($statement->getField()), + strtoupper($statement->getOperator()), + $placeHolder + ); + + // If this is the first fragment, remove the operator + if ($this->firstFragment()) { + $sql = $this->normalizer->removeInitialOperator($sql); + } + return new Criteria( - $statement, - $bindings + $sql, + array_map($this->normalizer->parseRawCallback(), $bindings) + ); + } + + /** + * Process a statement where the field is a Raw expression + * Gets forwarded through processSimpleCriteria() + * @param HasCriteria $statement + * @return Criteria + * @throws Exception + */ + protected function processObjectField(HasCriteria $statement): Criteria + { + // Normalize the field and parse if raw. + $field = $this->normalizer->normalizeField($statement->getField()); + $field = $field instanceof Raw + ? $this->normalizer->parseRaw($field) + : $field; + + /** @var HasCriteria */ + $statementType = get_class($statement); + $statement = new $statementType( + $field, + $statement->getOperator(), + $statement->getValue(), + $statement->getJoiner() + ); + return $this->processSimpleCriteria($statement); + } + + /** + * Process a statement where all is held as an expression in field only. + * @param HasCriteria $statement + * @return Criteria + */ + protected function processRawExpression(HasCriteria $statement): Criteria + { + // Ensure only raw can be used as Field. + if (! $statement->getField() instanceof Raw) { + throw new Exception(sprintf("Only Raw expressions can be processed, %s passed", json_encode($statement)), 1); + } + $field = $this->normalizer->parseRaw($statement->getField()); + $field = $this->normalizer->getTablePrefixer()->field($field); + + $sql = sprintf( + self::TEMPLATE_EXPRESSION, + $this->firstFragment() ? '' : \strtoupper($statement->getJoiner()), + ! $this->firstFragment() ? '' : strtoupper($statement->getCriteriaType()), + $field ); - // dump($values); TEMPLATE_IN + return new Criteria($sql, []); } } diff --git a/src/Exception/WpdbException.php b/src/Exception/WpdbException.php index 171c817..d5664ed 100644 --- a/src/Exception/WpdbException.php +++ b/src/Exception/WpdbException.php @@ -30,5 +30,4 @@ class WpdbException extends Exception { - } diff --git a/src/Parser/Normalizer.php b/src/Parser/Normalizer.php index ef954da..5662dff 100644 --- a/src/Parser/Normalizer.php +++ b/src/Parser/Normalizer.php @@ -209,8 +209,8 @@ private function normalizeJsonSelector(JsonSelector $selector, bool $isField = t /** * Normalizes a values to either a bindings or raw statement. * - * @param Raw|Binding|string|float|int|bool|null $value - * @return void + * @param Raw|Binding|JsonSelector|string|float|int|bool|null $value + * @return Raw|Binding */ public function normalizeValue($value) { @@ -220,6 +220,10 @@ public function normalizeValue($value) $value = $value->getValue(); break; + case is_string($value) && $this->jsonSelectors->isJsonSelector($value): + $value = Raw::val($this->normalizeJsonArrowSelector($value, false)); + break; + case is_string($value): $value = Binding::asString($value); break; @@ -237,15 +241,54 @@ public function normalizeValue($value) $value = $value; break; + case $value instanceof JsonSelector: + $value = Raw::val($this->normalizeJsonSelector($value, false)); + break; + + case is_null($value): + $value = Raw::val('NULL'); + break; + + + default: - // dump($value); - throw new Exception(\sprintf("Unexpected type :: %s", print_r($value, true)), 1); + throw new Exception(\sprintf("Unexpected type :: %s", json_encode($value)), 1); } return $value; } - /** + /** + * Normalizes a values to either a bindings or raw statement. + * + * @param Raw|\Closure|JsonSelector|string|null $value + * @return Raw|string|\Closure|null + */ + public function normalizeField($value) + { + switch (true) { + case is_string($value) && $this->jsonSelectors->isJsonSelector($value): + $value = Raw::val($this->normalizeJsonArrowSelector($value, true)); + break; + case $value instanceof JsonSelector: + $value = Raw::val($this->normalizeJsonSelector($value, true)); + break; + + case is_null($value): + case $value instanceof \Closure: + case $value instanceof Raw: + case is_string($value): + $value = $value; + break; + + default: + throw new Exception(\sprintf("Unexpected type :: %s", json_encode($value)), 1); + } + + return $value; + } + + /** * Attempts to parse a raw query, if bindings are defined then they will be bound first. * * @param Raw $raw @@ -259,6 +302,38 @@ public function parseRaw(Raw $raw): string : $this->wpdbHandler->interpolateQuery($raw->getValue(), $bindings); } + /** + * Returns a closure for parsing potential raw statements. + */ + public function parseRawCallback(): \Closure + { + /** + * @template M + * @param M|Raw $datum + * @return M + */ + return function ($datum) { + return $datum instanceof Raw ? $this->parseRaw($datum) : $datum; + }; + } + + /** + * Parses a valid type + * + * @param string|Raw|\Closure|null $value + * @return string + */ + public function normalizeForSQL($value): string + { + if ($value instanceof Raw) { + $value = $this->parseRaw($value); + } + if ($value instanceof \Closure || is_null($value)) { + throw new Exception(\sprintf("Field must be a valid type, %s supplied", json_encode($value)), 1); + } + return $value; + } + /** * Get access to the table prefixer. * @@ -268,4 +343,14 @@ public function getTablePrefixer(): TablePrefixer { return $this->tablePrefixer; } + + /** + * Removes the operator (AND|OR) from a statement. + * @param string $statement + * @return string + */ + public function removeInitialOperator(string $statement): string + { + return (string) (preg_replace(['#(?:AND|OR)#is', '/\s+/'], ' ', $statement, 1) ?: ''); + } } diff --git a/src/Parser/StatementParser.php b/src/Parser/StatementParser.php index 24ccd31..67847f7 100644 --- a/src/Parser/StatementParser.php +++ b/src/Parser/StatementParser.php @@ -28,9 +28,12 @@ use Pixie\Connection; use Pixie\WpdbHandler; +use Pixie\Criteria\Criteria; use Pixie\Parser\Normalizer; +use Pixie\Criteria\CriteriaBuilder; use Pixie\JSON\JsonSelectorHandler; use Pixie\Statement\TableStatement; +use Pixie\Statement\WhereStatement; use Pixie\Statement\SelectStatement; use Pixie\JSON\JsonExpressionFactory; use Pixie\Statement\GroupByStatement; @@ -201,8 +204,20 @@ public function parseOffset(?int $offset): string : ''; } - public function parseWhere(array $where): string + /** + * Parses an array of where statements into a Criteria model + * + * @param WhereStatement[]|mixed[] $where + * @return Criteria + */ + public function parseWhere(array $where): Criteria { - # code... + // Remove any none GroupByStatements + $where = array_filter($where, function ($statement): bool { + return is_a($statement, WhereStatement::class); + }); + $criteriaWhere = new CriteriaBuilder($this->connection); + $criteriaWhere->fromStatements($where); + return $criteriaWhere->getCriteria(); } } diff --git a/src/QueryBuilder/JsonQueryBuilder.php b/src/QueryBuilder/JsonQueryBuilder.php index a473a42..0be6696 100644 --- a/src/QueryBuilder/JsonQueryBuilder.php +++ b/src/QueryBuilder/JsonQueryBuilder.php @@ -2,6 +2,9 @@ namespace Pixie\QueryBuilder; +use Pixie\JSON\JsonSelector; +use Pixie\Statement\WhereStatement; + class JsonQueryBuilder extends QueryBuilderHandler { /** @@ -254,12 +257,19 @@ protected function whereJsonHandler($column, $nodes, $operator = null, $value = { // Handle potential raw values. if ($column instanceof Raw) { - $column = $this->adapterInstance->parseRaw($column); + $column = (string) $this->adapterInstance->parseRaw($column); } if ($nodes instanceof Raw) { - $nodes = $this->adapterInstance->parseRaw($nodes); + $nodes = (array) $this->adapterInstance->parseRaw($nodes); } + $this->statementBuilder->addWhere(new WhereStatement( + new JsonSelector($column, (array) $nodes), + $operator, + $value, + $joiner + )); + return $this->whereHandler( $this->jsonHandler->jsonExpressionFactory()->extractAndUnquote($column, $nodes), $operator, diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index fe83716..a466257 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -8,7 +8,6 @@ use Pixie\Binding; use Pixie\Exception; use Pixie\Connection; -use function mb_strlen; use Pixie\HasConnection; use Pixie\JSON\JsonHandler; use Pixie\QueryBuilder\Raw; @@ -25,6 +24,7 @@ use Pixie\Statement\GroupByStatement; use Pixie\Statement\OrderByStatement; use Pixie\Statement\StatementBuilder; +use function mb_strlen; class QueryBuilderHandler implements HasConnection { @@ -44,7 +44,7 @@ class QueryBuilderHandler implements HasConnection protected $statements = array(); /** @var StatementBuilder */ - protected $StatementBuilder; + protected $statementBuilder; /** * @var wpdb @@ -121,7 +121,7 @@ final public function __construct( $this->jsonHandler = new JsonHandler($connection); // Setup statement collection. - $this->StatementBuilder = new StatementBuilder(); + $this->statementBuilder = new StatementBuilder(); } /** @@ -530,7 +530,7 @@ public function getQuery(string $type = 'select', $dataToBePassed = array()) } if ('select' === $type) { - $queryArr = $this->adapterInstance->selectCol($this->StatementBuilder, array(), $this->statements); + $queryArr = $this->adapterInstance->selectCol($this->statementBuilder, array(), $this->statements); } else { $queryArr = $this->adapterInstance->$type($this->statements, $dataToBePassed); } @@ -738,7 +738,7 @@ public function table(...$tables) public function from(...$tables): self { foreach ($tables as $table) { - $this->StatementBuilder->addTable(new TableStatement($table)); + $this->statementBuilder->addTable(new TableStatement($table)); } $tables = $this->addTablePrefix($tables, false); $this->addStatement('tables', $tables); @@ -772,7 +772,7 @@ public function select($fields): self $statement = ! is_string($alias) ? new SelectStatement($field) : new SelectStatement($field, $alias); - $this->StatementBuilder->addSelect($statement); + $this->statementBuilder->addSelect($statement); } foreach ($fields as $field => $alias) { @@ -785,7 +785,7 @@ public function select($fields): self // $statement = is_numeric($field) // ? new SelectStatement($alias) // : new SelectStatement($field, $alias); - // $this->StatementBuilder->addSelect( + // $this->statementBuilder->addSelect( // $statement/* ->setIsDistinct($isDistinct) */ // ); @@ -794,7 +794,7 @@ public function select($fields): self if ($this->jsonHandler->isJsonSelector($field)) { /** @var string $field */ - $field = $this->jsonHandler->extractAndUnquoteFromJsonSelector($field); + $field = $this->jsonHandler->extractAndUnquoteFromJsonSelector($field); } $field = $this->addTablePrefix($field); @@ -818,7 +818,7 @@ public function selectDistinct($fields) { // $this->select($fields, true); // $this->addStatement('distinct', true); - $this->StatementBuilder->setDistinctSelect(true); + $this->statementBuilder->setDistinctSelect(true); $this->select(!is_array($fields) ? func_get_args() : $fields); return $this; } @@ -832,7 +832,7 @@ public function groupBy($field): self { $groupBys = is_array($field) ? $field : array( $field ); foreach (array_filter($groupBys, 'is_string') as $groupBy) { - $this->StatementBuilder->addGroupBy(new GroupByStatement($groupBy)); + $this->statementBuilder->addGroupBy(new GroupByStatement($groupBy)); } /** REMOVE BELOW IN V0.2 */ @@ -929,7 +929,7 @@ public function orderBy($fields, string $defaultDirection = 'ASC'): self if (is_int($column)) { continue; } - $this->StatementBuilder->addOrderBy( + $this->statementBuilder->addOrderBy( new OrderByStatement( $column, ! is_string($direction) ? $defaultDirection : (string) $direction @@ -980,7 +980,7 @@ public function orderByJson($key, $jsonKey, string $defaultDirection = 'ASC'): s public function limit(int $limit): self { $this->statements['limit'] = $limit; - $this->StatementBuilder->setLimit($limit); + $this->statementBuilder->setLimit($limit); return $this; } @@ -992,7 +992,7 @@ public function limit(int $limit): self public function offset(int $offset): self { $this->statements['offset'] = $offset; - $this->StatementBuilder->setOffset($offset); + $this->statementBuilder->setOffset($offset); return $this; } @@ -1039,7 +1039,14 @@ public function where($key, $operator = null, $value = null): self $operator = '='; } - $this->StatementBuilder->addWhere(new WhereStatement($key, $operator, $value, 'AND')); + $this->statementBuilder->addWhere( + new WhereStatement( + $this->jsonHandler->isJsonSelector($key) ? $this->jsonHandler->extractAndUnquoteFromJsonSelector($key) : $key, + $operator, + $value, + 'AND' + ) + ); return $this->whereHandler($key, $operator, $value); } @@ -1059,7 +1066,7 @@ public function orWhere($key, $operator = null, $value = null): self $operator = '='; } - $this->StatementBuilder->addWhere(new WhereStatement($key, $operator, $value, 'OR')); + $this->statementBuilder->addWhere(new WhereStatement($key, $operator, $value, 'OR')); return $this->whereHandler($key, $operator, $value, 'OR'); } @@ -1078,7 +1085,7 @@ public function whereNot($key, $operator = null, $value = null): self $operator = '='; } - $this->StatementBuilder->addWhere(new WhereStatement($key, $operator, $value, 'AND NOT')); + $this->statementBuilder->addWhere(new WhereStatement($key, $operator, $value, 'AND NOT')); return $this->whereHandler($key, $operator, $value, 'AND NOT'); } @@ -1097,7 +1104,7 @@ public function orWhereNot($key, $operator = null, $value = null) $operator = '='; } - $this->StatementBuilder->addWhere(new WhereStatement($key, $operator, $value, 'OR NOT')); + $this->statementBuilder->addWhere(new WhereStatement($key, $operator, $value, 'OR NOT')); return $this->whereHandler($key, $operator, $value, 'OR NOT'); } @@ -1109,7 +1116,7 @@ public function orWhereNot($key, $operator = null, $value = null) */ public function whereIn($key, $values): self { - $this->StatementBuilder->addWhere(new WhereStatement($key, 'IN', $values, 'AND')); + $this->statementBuilder->addWhere(new WhereStatement($key, 'IN', $values, 'AND')); return $this->whereHandler($key, 'IN', $values, 'AND'); } @@ -1121,7 +1128,7 @@ public function whereIn($key, $values): self */ public function whereNotIn($key, $values): self { - $this->StatementBuilder->addWhere(new WhereStatement($key, 'NOT IN', $values, 'AND')); + $this->statementBuilder->addWhere(new WhereStatement($key, 'NOT IN', $values, 'AND')); return $this->whereHandler($key, 'NOT IN', $values, 'AND'); } @@ -1133,7 +1140,7 @@ public function whereNotIn($key, $values): self */ public function orWhereIn($key, $values): self { - $this->StatementBuilder->addWhere(new WhereStatement($key, 'IN', $values, 'OR')); + $this->statementBuilder->addWhere(new WhereStatement($key, 'IN', $values, 'OR')); return $this->whereHandler($key, 'IN', $values, 'OR'); } @@ -1145,7 +1152,7 @@ public function orWhereIn($key, $values): self */ public function orWhereNotIn($key, $values): self { - $this->StatementBuilder->addWhere(new WhereStatement($key, 'NOT IN', $values, 'OR')); + $this->statementBuilder->addWhere(new WhereStatement($key, 'NOT IN', $values, 'OR')); return $this->whereHandler($key, 'NOT IN', $values, 'OR'); } @@ -1158,6 +1165,7 @@ public function orWhereNotIn($key, $values): self */ public function whereBetween($key, $valueFrom, $valueTo): self { + $this->statementBuilder->addWhere(new WhereStatement($key, 'BETWEEN', array( $valueFrom, $valueTo ), 'AND')); return $this->whereHandler($key, 'BETWEEN', array( $valueFrom, $valueTo ), 'AND'); } @@ -1170,6 +1178,7 @@ public function whereBetween($key, $valueFrom, $valueTo): self */ public function orWhereBetween($key, $valueFrom, $valueTo): self { + $this->statementBuilder->addWhere(new WhereStatement($key, 'BETWEEN', array( $valueFrom, $valueTo ), 'OR')); return $this->whereHandler($key, 'BETWEEN', array( $valueFrom, $valueTo ), 'OR'); } @@ -1583,7 +1592,6 @@ protected function whereHandler($key, $operator = null, $value = null, $joiner = } $this->statements['wheres'][] = compact('key', 'operator', 'value', 'joiner'); - // dump($this->statements['wheres']); return $this; } @@ -1699,6 +1707,6 @@ public function jsonBuilder(): JsonQueryBuilder */ public function getStatementBuilder(): StatementBuilder { - return $this->StatementBuilder; + return $this->statementBuilder; } } diff --git a/src/QueryBuilder/WPDBAdapter.php b/src/QueryBuilder/WPDBAdapter.php index b465a2b..ca026f5 100644 --- a/src/QueryBuilder/WPDBAdapter.php +++ b/src/QueryBuilder/WPDBAdapter.php @@ -6,18 +6,18 @@ use Pixie\Binding; use Pixie\Exception; -use function is_bool; - use Pixie\Connection; -use function is_float; - use Pixie\QueryBuilder\Raw; + use Pixie\Parser\StatementParser; + use Pixie\Criteria\CriteriaBuilder; use Pixie\Statement\SelectStatement; use Pixie\Statement\StatementBuilder; use Pixie\QueryBuilder\NestedCriteria; +use function is_bool; +use function is_float; class WPDBAdapter { @@ -60,12 +60,13 @@ public function selectCol(StatementBuilder $col, $data, $statements) // @phpstan // $selects = $this->arrayStr($statements['selects'], ', '); // Wheres - $criteriaWhere = new CriteriaBuilder($this->connection); - $criteriaWhere->fromStatements($col->getWhere()); + // $where = $parser->parseWhere($col->getWhere()); + $criteriaWhere = $parser->parseWhere($col->getWhere()); + // $criteriaWhere->fromStatements($col->getWhere()); list($whereCriteria, $whereBindings) = $this->buildCriteriaWithType($statements, 'wheres', 'WHERE'); - dump([$criteriaWhere->getCriteria()->getStatement(),$whereCriteria]); - dump([$criteriaWhere->getCriteria()->getBindings(), $whereBindings]); + // dump(['new' => $criteriaWhere->getStatement(),'old' => $whereCriteria]); + // dump(['new' => $criteriaWhere->getBindings(), 'old' => $whereBindings]); // Group bys // $groupBys = ''; @@ -80,10 +81,10 @@ public function selectCol(StatementBuilder $col, $data, $statements) // @phpstan // Having list($havingCriteria, $havingBindings) = $this->buildCriteriaWithType($statements, 'havings', 'HAVING'); - + dump($criteriaWhere->getBindings()); // Joins $joinString = $this->buildJoin($statements); -// dump($col->getWhere()); + // dump($col->getWhere()); /** @var string[] */ $sqlArray = [ 'SELECT' . ($col->getDistinctSelect() ? ' DISTINCT' : ''), @@ -91,7 +92,7 @@ public function selectCol(StatementBuilder $col, $data, $statements) // @phpstan 'FROM', $parser->parseTable($col->getTable()), $joinString, - $whereCriteria, + $criteriaWhere->getStatement(), $parser->parseGroupBy($col->getGroupBy()), $havingCriteria, $parser->parseOrderBy($col->getOrderBy()), @@ -101,7 +102,7 @@ public function selectCol(StatementBuilder $col, $data, $statements) // @phpstan $sql = $this->concatenateQuery($sqlArray); $bindings = array_merge( - $whereBindings, + $criteriaWhere->getBindings(), $havingBindings ); @@ -655,7 +656,7 @@ protected function buildCriteria(array $statements, bool $bindValues = true): ar } else { // Usual where like criteria if (!$bindValues) { - dump(7878789798798798798987); + // dump(7878789798798798798987); // Specially for joins // We are not binding values, lets sanitize then $value = $this->stringifyValue($this->wrapSanitizer($value)) ?? ''; diff --git a/src/Statement/HasCriteria.php b/src/Statement/HasCriteria.php index c388752..6ca5e18 100644 --- a/src/Statement/HasCriteria.php +++ b/src/Statement/HasCriteria.php @@ -26,9 +26,12 @@ namespace Pixie\Statement; +use Pixie\QueryBuilder\Raw; +use Pixie\JSON\JsonSelector; +use Pixie\QueryBuilder\QueryBuilderHandler; + interface HasCriteria { - public const WHERE_CRITERIA = 'WHERE'; public const JOIN_CRITERIA = 'JOIN'; @@ -56,7 +59,7 @@ public function getOperator(): string; /** * Get value for expression * - * @return string|int|float|bool|string[]|int[]|float[]|bool[]|null + * @return Raw|string|int|float|bool|Raw[]|string[]|int[]|float[]|bool[]|null */ public function getValue(); diff --git a/src/Statement/StatementBuilder.php b/src/Statement/StatementBuilder.php index b99b136..a797b2f 100644 --- a/src/Statement/StatementBuilder.php +++ b/src/Statement/StatementBuilder.php @@ -210,7 +210,7 @@ public function hasGroupBy(): bool return 0 < count($this->getGroupBy()); } - /** + /** * Adds a select statement to the collection. * * @param WhereStatement $statement diff --git a/src/Statement/WhereStatement.php b/src/Statement/WhereStatement.php index e9b6a49..a3581cc 100644 --- a/src/Statement/WhereStatement.php +++ b/src/Statement/WhereStatement.php @@ -39,7 +39,7 @@ class WhereStatement implements Statement, HasCriteria /** * The field which is being group by * - * @var string|\Closure(QueryBuilderHandler $query):void|Raw + * @var string|Raw|JsonSelector|\Closure(QueryBuilderHandler $query):void */ protected $field; @@ -67,7 +67,7 @@ class WhereStatement implements Statement, HasCriteria /** * Creates a Select Statement * - * @param string|\Closure(QueryBuilderHandler $query):void|Raw|JsonSelector $field + * @param string|Raw|JsonSelector|\Closure(QueryBuilderHandler $query):void $field * @param string $operator * @param string|int|float|bool|string[]|int[]|float[]|bool[] $value * @param string $joiner diff --git a/tests/Logable_WPDB.php b/tests/Logable_WPDB.php index f408848..1cf8214 100644 --- a/tests/Logable_WPDB.php +++ b/tests/Logable_WPDB.php @@ -92,6 +92,8 @@ public function prepare($query, ...$args) 'args' => $args[0], ); + dump($query, $args); + return sprintf(\str_replace('%s', "'%s'", $query), ...$args[0]); } diff --git a/tests/QueryBuilderHandler/TestIntegrationWithWPDB.php b/tests/QueryBuilderHandler/TestIntegrationWithWPDB.php index 2de8944..b06d109 100644 --- a/tests/QueryBuilderHandler/TestIntegrationWithWPDB.php +++ b/tests/QueryBuilderHandler/TestIntegrationWithWPDB.php @@ -134,8 +134,11 @@ public function queryBuilderProvider(?string $prefix = null, ?string $alias = nu return new QueryBuilderHandler($connection); } - /** @testdox [WPDB] It should be possible to do various simple SELECT queries using WHERE conditions using a live instance of WPDB (WHERE, WHERE NOT, WHERE AND, WHERE IN, WHERE BETWEEN) */ - public function testWhere() + /** + * @testdox [WPDB] It should be possible to do various simple SELECT queries using WHERE conditions using a live instance of WPDB (WHERE, WHERE NOT, WHERE AND, WHERE IN, WHERE BETWEEN) + * @group where + */ + public function testWhereIntegration() { $this->wpdb->insert('mock_foo', ['string' => 'a', 'number' => 1], ['%s', '%d']); $this->wpdb->insert('mock_foo', ['string' => 'a', 'number' => 2], ['%s', '%d']); @@ -195,7 +198,10 @@ public function testWhere() $this->assertEquals('3', $between[1]->number); } - /** @testdox [WPDB] It should be possible to do various Aggregation (COUNT, MIN, MAX, SUM, AVERAGE) for results, using WPDB*/ + /** + * @testdox [WPDB] It should be possible to do various Aggregation (COUNT, MIN, MAX, SUM, AVERAGE) for results, using WPDB + * @group where + */ public function testAggregation(): void { $this->wpdb->insert('mock_foo', ['string' => 'a', 'number' => 1], ['%s', '%d']); diff --git a/tests/QueryBuilderHandler/TestQueryBuilderSQLGeneration.php b/tests/QueryBuilderHandler/TestQueryBuilderSQLGeneration.php index ab081ad..c42d32b 100644 --- a/tests/QueryBuilderHandler/TestQueryBuilderSQLGeneration.php +++ b/tests/QueryBuilderHandler/TestQueryBuilderSQLGeneration.php @@ -461,7 +461,9 @@ public function testWhereAssumedEqualsOperator(): void ## GROUP, ORDER BY, LIMIT/OFFSET & HAVING ## ################################################ - /** @testdox It should be possible to create a grouped where condition */ + /** @testdox It should be possible to create a grouped where condition + * @group nested + */ public function testGroupedWhere(): void { $builder = $this->queryBuilderProvider() diff --git a/tests/QueryBuilderHandler/TestQueryBuilderWPDBPrepare.php b/tests/QueryBuilderHandler/TestQueryBuilderWPDBPrepare.php index eb3f2c3..9949264 100644 --- a/tests/QueryBuilderHandler/TestQueryBuilderWPDBPrepare.php +++ b/tests/QueryBuilderHandler/TestQueryBuilderWPDBPrepare.php @@ -123,7 +123,7 @@ public function testGetWithSingleConditionArrayInValue(): void $prepared = $this->wpdb->usage_log['prepare'][0]; // Check that the query is passed to prepare. - $this->assertEquals('SELECT * FROM foo WHERE key in (%d, %f, %s)', $prepared['query']); + $this->assertEquals('SELECT * FROM foo WHERE key IN (%d, %f, %s)', $prepared['query']); // Check values are used in order passed $this->assertEquals(2, $prepared['args'][0]); @@ -141,7 +141,7 @@ public function testGetWithSingleConditionBetweenValue(): void $prepared = $this->wpdb->usage_log['prepare'][0]; // Check that the query is passed to prepare. - $this->assertEquals('SELECT * FROM foo WHERE key between (%d, %f)', $prepared['query']); + $this->assertEquals('SELECT * FROM foo WHERE key BETWEEN (%d, %f)', $prepared['query']); // Check values are used in order passed $this->assertEquals(2, $prepared['args'][0]); @@ -163,7 +163,7 @@ public function testPreparesEvents(): void $prepared = $this->wpdb->usage_log['prepare'][0]; // Check that the query is passed to prepare. - $this->assertEquals('SELECT * FROM foo WHERE key between (%d, %f) AND status != %s', $prepared['query']); + $this->assertEquals('SELECT * FROM foo WHERE key BETWEEN (%d, %f) AND status != %s', $prepared['query']); // Check values are used in order passed $this->assertEquals(2, $prepared['args'][0]); diff --git a/tests/Unit/TestBinding.php b/tests/Unit/TestBinding.php index 641f8c1..d969185 100644 --- a/tests/Unit/TestBinding.php +++ b/tests/Unit/TestBinding.php @@ -157,7 +157,10 @@ public function testAsRaw(): void /** USING BINDING OBJECT */ - /** @testdox It should be possible to define both the value and its expected type, when creating a query using a Binding object. */ + /** @testdox It should be possible to define both the value and its expected type, when creating a query using a Binding object. + * @group where + * @group binding + */ public function testUsingBindingOnWhere(): void { $this->queryBuilderProvider() From e6b59640dcb0afdbdbf1d437cf1011e6032a689b Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Thu, 24 Feb 2022 00:48:30 +0000 Subject: [PATCH 21/24] having --- src/Parser/StatementParser.php | 18 +++ src/QueryBuilder/QueryBuilderHandler.php | 21 ++- src/QueryBuilder/WPDBAdapter.php | 42 +---- src/Statement/HasCriteria.php | 1 + src/Statement/HavingStatement.php | 153 ++++++++++++++++++ src/Statement/JoinStatement.php | 152 +++++++++++++++++ src/Statement/Statement.php | 1 + src/Statement/StatementBuilder.php | 35 ++++ tests/Logable_WPDB.php | 2 - .../TestQueryBuilderSQLGeneration.php | 12 +- 10 files changed, 393 insertions(+), 44 deletions(-) create mode 100644 src/Statement/HavingStatement.php create mode 100644 src/Statement/JoinStatement.php diff --git a/src/Parser/StatementParser.php b/src/Parser/StatementParser.php index 67847f7..ef48874 100644 --- a/src/Parser/StatementParser.php +++ b/src/Parser/StatementParser.php @@ -34,6 +34,7 @@ use Pixie\JSON\JsonSelectorHandler; use Pixie\Statement\TableStatement; use Pixie\Statement\WhereStatement; +use Pixie\Statement\HavingStatement; use Pixie\Statement\SelectStatement; use Pixie\JSON\JsonExpressionFactory; use Pixie\Statement\GroupByStatement; @@ -220,4 +221,21 @@ public function parseWhere(array $where): Criteria $criteriaWhere->fromStatements($where); return $criteriaWhere->getCriteria(); } + + /** + * Parses an array of where statements into a Criteria model + * + * @param HavingStatement[]|mixed[] $having + * @return Criteria + */ + public function parseHaving(array $having): Criteria + { + // Remove any none GroupByStatements + $having = array_filter($having, function ($statement): bool { + return is_a($statement, HavingStatement::class); + }); + $criteriaHaving = new CriteriaBuilder($this->connection); + $criteriaHaving->fromStatements($having); + return $criteriaHaving->getCriteria(); + } } diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index a466257..9315f3d 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -19,6 +19,7 @@ use Pixie\QueryBuilder\WPDBAdapter; use Pixie\Statement\TableStatement; use Pixie\Statement\WhereStatement; +use Pixie\Statement\HavingStatement; use Pixie\Statement\SelectStatement; use Pixie\QueryBuilder\TablePrefixer; use Pixie\Statement\GroupByStatement; @@ -997,7 +998,7 @@ public function offset(int $offset): self } /** - * @param string|string[]|Raw|Raw[] $key + * @param string|Raw|\Closure(QueryBuilderHandler):void $key * @param string $operator * @param mixed $value * @param string $joiner @@ -1006,6 +1007,18 @@ public function offset(int $offset): self */ public function having($key, string $operator, $value, string $joiner = 'AND') { + // If two params are given then assume operator is = + if (2 === func_num_args()) { + $value = $operator; + $operator = '='; + } + + $this->statementBuilder->addHaving( + new HavingStatement($key, $operator, $value, $joiner) + ); + + // dump($this->statementBuilder); + $key = $this->addTablePrefix($key); $this->statements['havings'][] = compact('key', 'operator', 'value', 'joiner'); @@ -1013,7 +1026,7 @@ public function having($key, string $operator, $value, string $joiner = 'AND') } /** - * @param string|string[]|Raw|Raw[] $key + * @param string|Raw|\Closure(QueryBuilderHandler):void $key * @param string $operator * @param mixed $value * @@ -1025,7 +1038,7 @@ public function orHaving($key, $operator, $value) } /** - * @param string|Raw $key + * @param string|Raw|\Closure(QueryBuilderHandler):void $key * @param string|mixed|null $operator Can be used as value, if 3rd arg not passed * @param mixed|null $value * @@ -1041,7 +1054,7 @@ public function where($key, $operator = null, $value = null): self $this->statementBuilder->addWhere( new WhereStatement( - $this->jsonHandler->isJsonSelector($key) ? $this->jsonHandler->extractAndUnquoteFromJsonSelector($key) : $key, + is_string($key) && $this->jsonHandler->isJsonSelector($key) ? $this->jsonHandler->extractAndUnquoteFromJsonSelector($key) : $key, $operator, $value, 'AND' diff --git a/src/QueryBuilder/WPDBAdapter.php b/src/QueryBuilder/WPDBAdapter.php index ca026f5..a33a1f1 100644 --- a/src/QueryBuilder/WPDBAdapter.php +++ b/src/QueryBuilder/WPDBAdapter.php @@ -50,41 +50,13 @@ public function selectCol(StatementBuilder $col, $data, $statements) // @phpstan if (!$col->hasSelect()) { $col->addSelect(new SelectStatement('*')); } - $_selects = $parser->parseSelect($col->getSelect()); - $_tables = $parser->parseTable($col->getTable()); + $where = $parser->parseWhere($col->getWhere()); + $having = $parser->parseHaving($col->getHaving()); - // From - // $tables = $this->arrayStr($statements['tables'], ', '); - // // Select - // $selects = $this->arrayStr($statements['selects'], ', '); - - // Wheres - // $where = $parser->parseWhere($col->getWhere()); - $criteriaWhere = $parser->parseWhere($col->getWhere()); - // $criteriaWhere->fromStatements($col->getWhere()); - - list($whereCriteria, $whereBindings) = $this->buildCriteriaWithType($statements, 'wheres', 'WHERE'); - // dump(['new' => $criteriaWhere->getStatement(),'old' => $whereCriteria]); - // dump(['new' => $criteriaWhere->getBindings(), 'old' => $whereBindings]); - - // Group bys - // $groupBys = ''; - // if (isset($statements['groupBys']) && $groupBys = $this->arrayStr($statements['groupBys'], ', ')) { - // $groupBys = 'GROUP BY ' . $groupBys; - // } - - - // Limit and offset - // $limit = isset($statements['limit']) ? 'LIMIT ' . (int) $statements['limit'] : ''; - // $offset = isset($statements['offset']) ? 'OFFSET ' . (int) $statements['offset'] : ''; - - // Having - list($havingCriteria, $havingBindings) = $this->buildCriteriaWithType($statements, 'havings', 'HAVING'); - dump($criteriaWhere->getBindings()); // Joins $joinString = $this->buildJoin($statements); - // dump($col->getWhere()); + /** @var string[] */ $sqlArray = [ 'SELECT' . ($col->getDistinctSelect() ? ' DISTINCT' : ''), @@ -92,9 +64,9 @@ public function selectCol(StatementBuilder $col, $data, $statements) // @phpstan 'FROM', $parser->parseTable($col->getTable()), $joinString, - $criteriaWhere->getStatement(), + $where->getStatement(), $parser->parseGroupBy($col->getGroupBy()), - $havingCriteria, + $having->getStatement(), $parser->parseOrderBy($col->getOrderBy()), $parser->parseLimit($col->getLimit()), $parser->parseOffset($col->getOffset()), @@ -102,8 +74,8 @@ public function selectCol(StatementBuilder $col, $data, $statements) // @phpstan $sql = $this->concatenateQuery($sqlArray); $bindings = array_merge( - $criteriaWhere->getBindings(), - $havingBindings + $where->getBindings(), + $having->getBindings() ); return compact('sql', 'bindings'); diff --git a/src/Statement/HasCriteria.php b/src/Statement/HasCriteria.php index 6ca5e18..ffa48fb 100644 --- a/src/Statement/HasCriteria.php +++ b/src/Statement/HasCriteria.php @@ -34,6 +34,7 @@ interface HasCriteria { public const WHERE_CRITERIA = 'WHERE'; public const JOIN_CRITERIA = 'JOIN'; + public const HAVING_CRITERIA = 'HAVING'; /** * Returns the type of criteria (JOIN, WHERE, HAVING) diff --git a/src/Statement/HavingStatement.php b/src/Statement/HavingStatement.php new file mode 100644 index 0000000..c7ff785 --- /dev/null +++ b/src/Statement/HavingStatement.php @@ -0,0 +1,153 @@ + + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @package Gin0115\Pixie + * @subpackage QueryBuilder\Statement + */ + +namespace Pixie\Statement; + +use Closure; +use TypeError; +use Pixie\QueryBuilder\Raw; +use Pixie\JSON\JsonSelector; +use Pixie\Statement\Statement; +use Pixie\Statement\HasCriteria; +use Pixie\QueryBuilder\QueryBuilderHandler; + +class HavingStatement implements Statement, HasCriteria +{ + /** + * The field which is being group by + * + * @var string|Raw|JsonSelector|\Closure(QueryBuilderHandler $query):void $field + */ + protected $field; + + /** + * The operator + * + * @var string + */ + protected $operator; + + /** + * Value for expression + * + * @var string|int|float|bool|string[]|int[]|float[]|bool[]|null + */ + protected $value; + + /** + * Joiner + * + * @var string + */ + protected $joiner; + + /** + * Creates a Select Statement + * + * @param string|Raw|JsonSelector|\Closure(QueryBuilderHandler $query):void $field + * @param string $operator + * @param string|int|float|bool|string[]|int[]|float[]|bool[] $value + * @param string $joiner + */ + public function __construct($field, $operator = null, $value = null, string $joiner = 'AND') + { + // Verify valid field type. + $this->verifyField($field); + $this->field = $field; + $this->operator = $operator ?? '='; + $this->value = $value; + $this->joiner = $joiner; + } + + /** @inheritDoc */ + public function getCriteriaType(): string + { + return HasCriteria::HAVING_CRITERIA; + } + + /** @inheritDoc */ + public function getType(): string + { + return Statement::HAVING; + } + + /** + * Verifies if the passed filed is of a valid type. + * + * @param mixed $field + * @return void + */ + protected function verifyField($field): void + { + if ( + !is_string($field) + && ! is_a($field, Raw::class) + && !is_a($field, JsonSelector::class) + && !is_a($field, \Closure::class) + ) { + throw new TypeError("Only strings, Raw, JsonSelector and Closures may be used as fields in Where statements."); + } + } + /** + * Gets the field. + * + * @return string|\Closure(QueryBuilderHandler $query):void|Raw|JsonSelector + */ + public function getField() + { + return $this->field; + } + + /** + * Get the operator + * + * @return string + */ + public function getOperator(): string + { + return $this->operator; + } + + /** + * Get value for expression + * + * @return string|int|float|bool|string[]|int[]|float[]|bool[]|null + */ + public function getValue() + { + return $this->value; + } + + /** + * Get joiner + * + * @return string + */ + public function getJoiner(): string + { + return $this->joiner; + } +} diff --git a/src/Statement/JoinStatement.php b/src/Statement/JoinStatement.php new file mode 100644 index 0000000..8921d4d --- /dev/null +++ b/src/Statement/JoinStatement.php @@ -0,0 +1,152 @@ + + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @package Gin0115\Pixie + * @subpackage QueryBuilder\Statement + */ + +namespace Pixie\Statement; + +use Closure; +use TypeError; +use Pixie\QueryBuilder\Raw; +use Pixie\JSON\JsonSelector; +use Pixie\Statement\Statement; +use Pixie\Statement\HasCriteria; + +class JoinStatement implements Statement, HasCriteria +{ + /** + * The field which is being group by + * + * @var string|Raw|JsonSelector|\Closure(QueryBuilderHandler $query):void + */ + protected $field; + + /** + * The operator + * + * @var string + */ + protected $operator; + + /** + * Value for expression + * + * @var string|int|float|bool|string[]|int[]|float[]|bool[]|null + */ + protected $value; + + /** + * Joiner + * + * @var string + */ + protected $joiner; + + /** + * Creates a Select Statement + * + * @param string|Raw|JsonSelector|\Closure(QueryBuilderHandler $query):void $field + * @param string $operator + * @param string|int|float|bool|string[]|int[]|float[]|bool[] $value + * @param string $joiner + */ + public function __construct($field, $operator = null, $value = null, string $joiner = 'AND') + { + // Verify valid field type. + $this->verifyField($field); + $this->field = $field; + $this->operator = $operator ?? '='; + $this->value = $value; + $this->joiner = $joiner; + } + + /** @inheritDoc */ + public function getCriteriaType(): string + { + return HasCriteria::HAVING_CRITERIA; + } + + /** @inheritDoc */ + public function getType(): string + { + return Statement::HAVING; + } + + /** + * Verifies if the passed filed is of a valid type. + * + * @param mixed $field + * @return void + */ + protected function verifyField($field): void + { + if ( + !is_string($field) + && ! is_a($field, Raw::class) + && !is_a($field, JsonSelector::class) + && !is_a($field, \Closure::class) + ) { + throw new TypeError("Only strings, Raw, JsonSelector and Closures may be used as fields in Where statements."); + } + } + /** + * Gets the field. + * + * @return string|\Closure(QueryBuilderHandler $query):void|Raw|JsonSelector + */ + public function getField() + { + return $this->field; + } + + /** + * Get the operator + * + * @return string + */ + public function getOperator(): string + { + return $this->operator; + } + + /** + * Get value for expression + * + * @return string|int|float|bool|string[]|int[]|float[]|bool[]|null + */ + public function getValue() + { + return $this->value; + } + + /** + * Get joiner + * + * @return string + */ + public function getJoiner(): string + { + return $this->joiner; + } +} diff --git a/src/Statement/Statement.php b/src/Statement/Statement.php index 4e848c7..beae3c6 100644 --- a/src/Statement/Statement.php +++ b/src/Statement/Statement.php @@ -36,6 +36,7 @@ interface Statement public const ORDER_BY = 'orderby'; public const GROUP_BY = 'groupby'; public const WHERE = 'where'; + public const HAVING = 'having'; /** * Get the statement type diff --git a/src/Statement/StatementBuilder.php b/src/Statement/StatementBuilder.php index a797b2f..a86e125 100644 --- a/src/Statement/StatementBuilder.php +++ b/src/Statement/StatementBuilder.php @@ -39,6 +39,7 @@ class StatementBuilder * orderby: OrderByStatement[], * groupby: GroupByStatement[], * where: WhereStatement[], + * having: HavingStatement[], * } */ protected $statements = [ @@ -47,6 +48,7 @@ class StatementBuilder Statement::ORDER_BY => [], Statement::GROUP_BY => [], Statement::WHERE => [], + Statement::HAVING => [] ]; /** @@ -75,6 +77,7 @@ class StatementBuilder * orderby: OrderByStatement[], * groupby: GroupByStatement[], * where: WhereStatement[], + * having: HavingStatement[], *} */ public function getStatements(): array @@ -242,6 +245,38 @@ public function hasWhere(): bool return 0 < count($this->getWhere()); } + /** + * Adds a select statement to the collection. + * + * @param HavingStatement $statement + * @return self + */ + public function addHaving(HavingStatement $statement): self + { + $this->statements[Statement::HAVING][] = $statement; + return $this; + } + + /** + * Get all HavingStatements + * + * @return HavingStatement[] + */ + public function getHaving(): array + { + return $this->statements[Statement::HAVING]; + } + + /** + * Having statements exist. + * + * @return bool + */ + public function hasHaving(): bool + { + return 0 < count($this->getHaving()); + } + /** * Set denotes if a DISTINCT SELECT * diff --git a/tests/Logable_WPDB.php b/tests/Logable_WPDB.php index 1cf8214..f408848 100644 --- a/tests/Logable_WPDB.php +++ b/tests/Logable_WPDB.php @@ -92,8 +92,6 @@ public function prepare($query, ...$args) 'args' => $args[0], ); - dump($query, $args); - return sprintf(\str_replace('%s', "'%s'", $query), ...$args[0]); } diff --git a/tests/QueryBuilderHandler/TestQueryBuilderSQLGeneration.php b/tests/QueryBuilderHandler/TestQueryBuilderSQLGeneration.php index c42d32b..fd9f730 100644 --- a/tests/QueryBuilderHandler/TestQueryBuilderSQLGeneration.php +++ b/tests/QueryBuilderHandler/TestQueryBuilderSQLGeneration.php @@ -610,7 +610,10 @@ public function testOrderByMultiple(): void $this->assertValidSQL($builderDesc->getQuery()->getRawSql()); } - /** @testdox It should be possible to set HAVING in queries. */ + /** + * @testdox It should be possible to set HAVING in queries. + * @group having + */ public function testHaving(): void { $builderHaving = $this->queryBuilderProvider() @@ -1061,7 +1064,7 @@ public function testSubQueryInOperatorExample() $builder = $this->queryBuilderProvider(); $avgSubQuery = $builder->table('orders')->selectDistinct("customerNumber"); - + $builder->table('customers') ->select('customerName') ->whereNotIn('customerNumber', $builder->subQuery($avgSubQuery)) @@ -1208,7 +1211,10 @@ public function testCount_OrderBy_GroupBy_Complex(): void $this->assertValidSQL($sql); } - /** @testdox Examples used in WIKI for having(). */ + /** + * @testdox Examples used in WIKI for having(). + * @group having + */ public function testHavingExamplesFromWiki(): void { // Using SUM function From 38d6326d4ffd4be0e7d73b9d6004094924d743b2 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Sun, 27 Feb 2022 00:14:48 +0000 Subject: [PATCH 22/24] Join statement done --- src/{Criteria => Parser}/Criteria.php | 4 +- src/{Criteria => Parser}/CriteriaBuilder.php | 4 +- src/Parser/JoinBuilder.php | 32 +++++++ src/Parser/Normalizer.php | 15 +++- src/Parser/StatementParser.php | 56 +++++++++++-- src/QueryBuilder/JoinBuilder.php | 15 ++++ src/QueryBuilder/QueryBuilderHandler.php | 10 ++- src/QueryBuilder/WPDBAdapter.php | 22 ++--- src/Statement/JoinStatement.php | 84 ++++++++++++------- src/Statement/Statement.php | 9 +- src/Statement/StatementBuilder.php | 48 ++++++++++- .../TestQueryBuilderSQLGeneration.php | 51 +++++++++-- tests/Unit/Critiera/TestCritieraBuilder.php | 3 +- 13 files changed, 276 insertions(+), 77 deletions(-) rename src/{Criteria => Parser}/Criteria.php (97%) rename src/{Criteria => Parser}/CriteriaBuilder.php (99%) create mode 100644 src/Parser/JoinBuilder.php diff --git a/src/Criteria/Criteria.php b/src/Parser/Criteria.php similarity index 97% rename from src/Criteria/Criteria.php rename to src/Parser/Criteria.php index 7687f54..11f177c 100644 --- a/src/Criteria/Criteria.php +++ b/src/Parser/Criteria.php @@ -21,10 +21,10 @@ * @author Glynn Quelch * @license http://www.opensource.org/licenses/mit-license.html MIT License * @package Gin0115\Pixie - * @subpackage Criteria + * @subpackage Parser */ -namespace Pixie\Criteria; +namespace Pixie\Parser; class Criteria { diff --git a/src/Criteria/CriteriaBuilder.php b/src/Parser/CriteriaBuilder.php similarity index 99% rename from src/Criteria/CriteriaBuilder.php rename to src/Parser/CriteriaBuilder.php index 828c38f..7b78841 100644 --- a/src/Criteria/CriteriaBuilder.php +++ b/src/Parser/CriteriaBuilder.php @@ -21,10 +21,10 @@ * @author Glynn Quelch * @license http://www.opensource.org/licenses/mit-license.html MIT License * @package Gin0115\Pixie - * @subpackage Criteria + * @subpackage Parser */ -namespace Pixie\Criteria; +namespace Pixie\Parser; use Pixie\Binding; use Pixie\Exception; diff --git a/src/Parser/JoinBuilder.php b/src/Parser/JoinBuilder.php new file mode 100644 index 0000000..889d2ac --- /dev/null +++ b/src/Parser/JoinBuilder.php @@ -0,0 +1,32 @@ + + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @package Gin0115\Pixie + * @subpackage Parser + */ + +namespace Pixie\Parser; + +class JoinBuilder { + + // $this->c +} \ No newline at end of file diff --git a/src/Parser/Normalizer.php b/src/Parser/Normalizer.php index 5662dff..22b0c9e 100644 --- a/src/Parser/Normalizer.php +++ b/src/Parser/Normalizer.php @@ -32,11 +32,14 @@ use Pixie\QueryBuilder\Raw; use Pixie\JSON\JsonSelector; use Pixie\Parser\TablePrefixer; +use Pixie\Statement\JoinStatement; use Pixie\JSON\JsonSelectorHandler; +use Pixie\QueryBuilder\JoinBuilder; use Pixie\Statement\TableStatement; use Pixie\Statement\SelectStatement; use Pixie\JSON\JsonExpressionFactory; use Pixie\Statement\OrderByStatement; +use Pixie\QueryBuilder\JsonQueryBuilder; class Normalizer { @@ -156,7 +159,17 @@ public function orderByStatement(OrderByStatement $statement): string */ public function tableStatement(TableStatement $statement): string { - $table = $statement->getTable(); + return $this->normalizeTable($statement->getTable()); + } + + /** + * Casts a table (string or Raw) including table prefixing. + * + * @var string|Raw $table + * @return string + */ + public function normalizeTable($table): string + { return is_a($table, Raw::class) ? $this->normalizeRaw($table) : $this->tablePrefixer->table($table) ?? $table; diff --git a/src/Parser/StatementParser.php b/src/Parser/StatementParser.php index ef48874..43ba278 100644 --- a/src/Parser/StatementParser.php +++ b/src/Parser/StatementParser.php @@ -28,10 +28,12 @@ use Pixie\Connection; use Pixie\WpdbHandler; -use Pixie\Criteria\Criteria; +use Pixie\Parser\Criteria; use Pixie\Parser\Normalizer; -use Pixie\Criteria\CriteriaBuilder; +use Pixie\Parser\CriteriaBuilder; +use Pixie\Statement\JoinStatement; use Pixie\JSON\JsonSelectorHandler; +use Pixie\QueryBuilder\JoinBuilder; use Pixie\Statement\TableStatement; use Pixie\Statement\WhereStatement; use Pixie\Statement\HavingStatement; @@ -47,6 +49,7 @@ class StatementParser protected const TEMPLATE_GROUP_BY = "GROUP BY %s"; protected const TEMPLATE_LIMIT = "LIMIT %d"; protected const TEMPLATE_OFFSET = "OFFSET %d"; + protected const TEMPLATE_JOIN = "%s JOIN %s ON %s"; /** * @var Connection @@ -122,10 +125,7 @@ public function parseTable(array $tables): string return is_a($statement, TableStatement::class); }); - $tables = array_map(function (TableStatement $table): string { - return $this->normalizer->tableStatement($table); - }, $tables); - + $tables = array_map([$this->normalizer,'tableStatement'], $tables); return join(', ', $tables); } @@ -238,4 +238,48 @@ public function parseHaving(array $having): Criteria $criteriaHaving->fromStatements($having); return $criteriaHaving->getCriteria(); } + + /** + * Parses an array of Join statements + * + * @param array $join + * @return string + */ + public function parseJoin(array $join): string + { + // @var JoinStatement[] $join + $join = array_filter($join, function ($statement): bool { + return is_a($statement, JoinStatement::class); + }); + + // Cast to string, with or without alias, + $joins = array_map(function (JoinStatement $statement): string { + + // Extract the table and possible alias. + ['key' => $table, 'value' => $alias] = $statement->getTable(); + $table = $this->normalizer->normalizeTable($table); + + // If not already a closure, cast to. + $key = $statement->getField() instanceof \Closure + ? $statement->getField() + : JoinBuilder::createClosure( + $this->normalizer->normalizeField($statement->getField()), + $statement->getOperator(), + $this->normalizer->normalizeField($statement->getValue()) + ); + + // Populate the join builder + $builder = new JoinBuilder($this->connection); + $key($builder); + + return sprintf( + self::TEMPLATE_JOIN, + strtoupper($statement->getJoinType()), + 0 === $alias ? $table : sprintf(self::TEMPLATE_AS, $table, $alias), + $builder->getQuery('criteriaOnly', false)->getSql() + ); + }, $join); + + return join(' ', $joins); + } } diff --git a/src/QueryBuilder/JoinBuilder.php b/src/QueryBuilder/JoinBuilder.php index 3e18421..0b5a0df 100644 --- a/src/QueryBuilder/JoinBuilder.php +++ b/src/QueryBuilder/JoinBuilder.php @@ -4,6 +4,21 @@ class JoinBuilder extends QueryBuilderHandler { + /** + * Returns the closure used to create a join statement. + * + * @param string|Raw|Closure $key + * @param string|null $operator + * @param mixed $value + * @return \Closure(JoinBuilder $joinBuilder):void + */ + public static function createClosure($key, $operator, $value): \Closure + { + return function (JoinBuilder $joinBuilder) use ($key, $operator, $value): void { + $joinBuilder->on($key, $operator, $value); + }; + } + /** * @param string|Raw $key * @param string|null $operator diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index 9315f3d..80416f3 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -8,11 +8,13 @@ use Pixie\Binding; use Pixie\Exception; use Pixie\Connection; +use function mb_strlen; use Pixie\HasConnection; use Pixie\JSON\JsonHandler; use Pixie\QueryBuilder\Raw; use Pixie\JSON\JsonSelector; use Pixie\Hydration\Hydrator; +use Pixie\Statement\JoinStatement; use Pixie\QueryBuilder\JoinBuilder; use Pixie\QueryBuilder\QueryObject; use Pixie\QueryBuilder\Transaction; @@ -25,7 +27,6 @@ use Pixie\Statement\GroupByStatement; use Pixie\Statement\OrderByStatement; use Pixie\Statement\StatementBuilder; -use function mb_strlen; class QueryBuilderHandler implements HasConnection { @@ -1017,8 +1018,6 @@ public function having($key, string $operator, $value, string $joiner = 'AND') new HavingStatement($key, $operator, $value, $joiner) ); - // dump($this->statementBuilder); - $key = $this->addTablePrefix($key); $this->statements['havings'][] = compact('key', 'operator', 'value', 'joiner'); @@ -1419,6 +1418,11 @@ protected function handleTransactionCall(Closure $callback, Transaction $transac */ public function join($table, $key, ?string $operator = null, $value = null, $type = 'inner') { + $table1 = $this->maybeFlipArrayValues(is_array($table) ? $table : [$table]); + $this->statementBuilder->addStatement( + new JoinStatement(end($table1) , $key, $operator, $value, $type) + ); + // Potentially cast key from JSON if ($this->jsonHandler->isJsonSelector($key)) { /** @var string $key */ diff --git a/src/QueryBuilder/WPDBAdapter.php b/src/QueryBuilder/WPDBAdapter.php index a33a1f1..f5e02f8 100644 --- a/src/QueryBuilder/WPDBAdapter.php +++ b/src/QueryBuilder/WPDBAdapter.php @@ -6,18 +6,18 @@ use Pixie\Binding; use Pixie\Exception; +use function is_bool; + use Pixie\Connection; -use Pixie\QueryBuilder\Raw; +use function is_float; +use Pixie\QueryBuilder\Raw; +use Pixie\Parser\CriteriaBuilder; use Pixie\Parser\StatementParser; - -use Pixie\Criteria\CriteriaBuilder; use Pixie\Statement\SelectStatement; use Pixie\Statement\StatementBuilder; use Pixie\QueryBuilder\NestedCriteria; -use function is_bool; -use function is_float; class WPDBAdapter { @@ -53,9 +53,7 @@ public function selectCol(StatementBuilder $col, $data, $statements) // @phpstan $where = $parser->parseWhere($col->getWhere()); $having = $parser->parseHaving($col->getHaving()); - - // Joins - $joinString = $this->buildJoin($statements); + $join = $parser->parseJoin($col->getJoin()); /** @var string[] */ $sqlArray = [ @@ -63,7 +61,7 @@ public function selectCol(StatementBuilder $col, $data, $statements) // @phpstan $parser->parseSelect($col->getSelect()), 'FROM', $parser->parseTable($col->getTable()), - $joinString, + $parser->parseJoin($col->getJoin()), $where->getStatement(), $parser->parseGroupBy($col->getGroupBy()), $having->getStatement(), @@ -573,7 +571,6 @@ protected function buildCriteria(array $statements, bool $bindValues = true): ar // Merge the bindings we get from nestedCriteria object $bindings = array_merge($bindings, $queryObject->getBindings()); - // dump($statement['joiner'], $statement); // Append the sql we get from the nestedCriteria object $criteria .= $statement['joiner'] . ' (' . $queryObject->getSql() . ') '; @@ -628,7 +625,6 @@ protected function buildCriteria(array $statements, bool $bindValues = true): ar } else { // Usual where like criteria if (!$bindValues) { - // dump(7878789798798798798987); // Specially for joins // We are not binding values, lets sanitize then $value = $this->stringifyValue($this->wrapSanitizer($value)) ?? ''; @@ -750,8 +746,8 @@ protected function buildJoin(array $statements): string foreach ($statements['joins'] as $joinArr) { if (is_array($joinArr['table'])) { - $mainTable = $this->stringifyValue($this->wrapSanitizer($joinArr['table'][0])); - $aliasTable = $this->stringifyValue($this->wrapSanitizer($joinArr['table'][1])); + $mainTable = $this->stringifyValue($this->wrapSanitizer(array_keys($joinArr['table'])[0])); + $aliasTable = $this->stringifyValue($this->wrapSanitizer(array_values($joinArr['table'])[0])); $table = $mainTable . ' AS ' . $aliasTable; } else { $table = $joinArr['table'] instanceof Raw diff --git a/src/Statement/JoinStatement.php b/src/Statement/JoinStatement.php index 8921d4d..9bdbcf8 100644 --- a/src/Statement/JoinStatement.php +++ b/src/Statement/JoinStatement.php @@ -32,13 +32,21 @@ use Pixie\JSON\JsonSelector; use Pixie\Statement\Statement; use Pixie\Statement\HasCriteria; +use Pixie\QueryBuilder\JoinBuilder; -class JoinStatement implements Statement, HasCriteria +class JoinStatement implements Statement { + /** + * The table which is being group by + * + * @var string|Raw|array + */ + protected $table; + /** * The field which is being group by * - * @var string|Raw|JsonSelector|\Closure(QueryBuilderHandler $query):void + * @var string|Raw|JsonSelector|\Closure(JoinBuilder $joinBuilder):void */ protected $field; @@ -57,67 +65,69 @@ class JoinStatement implements Statement, HasCriteria protected $value; /** - * Joiner + * Type of join * * @var string */ - protected $joiner; + protected $joinType; + /** * Creates a Select Statement * - * @param string|Raw|JsonSelector|\Closure(QueryBuilderHandler $query):void $field + * @param string|Raw|array $table + * @param string|Raw|JsonSelector|\Closure(JoinBuilder $joinBuilder):void $field * @param string $operator * @param string|int|float|bool|string[]|int[]|float[]|bool[] $value - * @param string $joiner + * @param string $joinType */ - public function __construct($field, $operator = null, $value = null, string $joiner = 'AND') - { - // Verify valid field type. - $this->verifyField($field); + public function __construct( + $table, + $field, + $operator = null, + $value = null, + string $joinType = 'INNER' + ) { + // Verify valid table type. + $this->verifyTable($table); + $this->table = $table; $this->field = $field; $this->operator = $operator ?? '='; $this->value = $value; - $this->joiner = $joiner; + $this->joinType = $joinType; } - /** @inheritDoc */ - public function getCriteriaType(): string - { - return HasCriteria::HAVING_CRITERIA; - } /** @inheritDoc */ public function getType(): string { - return Statement::HAVING; + return Statement::JOIN; } /** * Verifies if the passed filed is of a valid type. * - * @param mixed $field + * @param mixed $table * @return void */ - protected function verifyField($field): void + protected function verifyTable($table): void { if ( - !is_string($field) - && ! is_a($field, Raw::class) - && !is_a($field, JsonSelector::class) - && !is_a($field, \Closure::class) + !is_string($table) + && !is_array($table) + && ! is_a($table, Raw::class) ) { - throw new TypeError("Only strings, Raw, JsonSelector and Closures may be used as fields in Where statements."); + throw new TypeError("Only strings and Raw may be used as tables in Where statements."); } } /** - * Gets the field. + * Gets the table. * - * @return string|\Closure(QueryBuilderHandler $query):void|Raw|JsonSelector + * @return string|Raw|array */ - public function getField() + public function getTable() { - return $this->field; + return $this->table; } /** @@ -133,7 +143,7 @@ public function getOperator(): string /** * Get value for expression * - * @return string|int|float|bool|string[]|int[]|float[]|bool[]|null + * @return string|Raw|null */ public function getValue() { @@ -141,12 +151,22 @@ public function getValue() } /** - * Get joiner + * Get field + * + * @return string|\Closure(JoinBuilder $query):void|Raw|JsonSelector + */ + public function getField() + { + return $this->field; + } + + /** + * Get the join type. * * @return string */ - public function getJoiner(): string + public function getJoinType(): string { - return $this->joiner; + return $this->joinType; } } diff --git a/src/Statement/Statement.php b/src/Statement/Statement.php index beae3c6..f25ad46 100644 --- a/src/Statement/Statement.php +++ b/src/Statement/Statement.php @@ -31,12 +31,13 @@ interface Statement /** * Statement Types. */ - public const SELECT = 'select'; - public const TABLE = 'table'; + public const SELECT = 'select'; + public const TABLE = 'table'; public const ORDER_BY = 'orderby'; public const GROUP_BY = 'groupby'; - public const WHERE = 'where'; - public const HAVING = 'having'; + public const WHERE = 'where'; + public const HAVING = 'having'; + public const JOIN = 'join'; /** * Get the statement type diff --git a/src/Statement/StatementBuilder.php b/src/Statement/StatementBuilder.php index a86e125..63264f0 100644 --- a/src/Statement/StatementBuilder.php +++ b/src/Statement/StatementBuilder.php @@ -40,15 +40,17 @@ class StatementBuilder * groupby: GroupByStatement[], * where: WhereStatement[], * having: HavingStatement[], + * join: JoinStatement[], * } */ protected $statements = [ - Statement::SELECT => [], - Statement::TABLE => [], + Statement::SELECT => [], + Statement::TABLE => [], Statement::ORDER_BY => [], Statement::GROUP_BY => [], - Statement::WHERE => [], - Statement::HAVING => [] + Statement::WHERE => [], + Statement::HAVING => [], + Statement::JOIN => [], ]; /** @@ -78,6 +80,7 @@ class StatementBuilder * groupby: GroupByStatement[], * where: WhereStatement[], * having: HavingStatement[], + * join:JoinStatement[] *} */ public function getStatements(): array @@ -85,6 +88,17 @@ public function getStatements(): array return $this->statements; } + /** + * Adds a statement to the collection based on its type. + * @param Statement $statement + * @return StatementBuilder + */ + public function addStatement(Statement $statement): self + { + $this->statements[$statement->getType()][] = $statement; + return $this; + } + /** * Adds a select statement to the collection. * @@ -97,6 +111,12 @@ public function addSelect(SelectStatement $statement): self return $this; } + public function has(string $statementType): bool + { + return \array_key_exists($statementType, $this->statements) + && 0 !== count($this->statements[$statementType]); + } + /** * Get all SelectStatements * @@ -277,6 +297,26 @@ public function hasHaving(): bool return 0 < count($this->getHaving()); } + /** + * Get all JoinStatements + * + * @return JoinStatement[] + */ + public function getJoin(): array + { + return $this->statements[Statement::JOIN]; + } + + /** + * Join statements exist. + * + * @return bool + */ + public function hasJoin(): bool + { + return 0 < count($this->getJoin()); + } + /** * Set denotes if a DISTINCT SELECT * diff --git a/tests/QueryBuilderHandler/TestQueryBuilderSQLGeneration.php b/tests/QueryBuilderHandler/TestQueryBuilderSQLGeneration.php index fd9f730..a0b9522 100644 --- a/tests/QueryBuilderHandler/TestQueryBuilderSQLGeneration.php +++ b/tests/QueryBuilderHandler/TestQueryBuilderSQLGeneration.php @@ -668,7 +668,10 @@ public function testOffset() ## JOIN {INNER, LEFT, RIGHT, FULL OUTER} ## ################################################# - /** @testdox It should be possible to create a query using (INNER) join for a relationship */ + /** + * @testdox It should be possible to create a query using (INNER) join for a relationship + * @group join + */ public function testJoin(): void { // Single Condition @@ -682,7 +685,10 @@ public function testJoin(): void $this->assertValidSQL($builder->getQuery()->getRawSql()); } - /** @testdox It should be possible to create a query using (OUTER) join for a relationship */ + /** + * @testdox It should be possible to create a query using (OUTER) join for a relationship + * @group join + */ public function testOuterJoin() { // Single Condition @@ -696,7 +702,10 @@ public function testOuterJoin() $this->assertValidSQL($builder->getQuery()->getRawSql()); } - /** @testdox It should be possible to create a query using (RIGHT) join for a relationship */ + /** + * @testdox It should be possible to create a query using (RIGHT) join for a relationship + * @group join + */ public function testRightJoin() { // Single Condition @@ -710,7 +719,10 @@ public function testRightJoin() $this->assertValidSQL($builder->getQuery()->getRawSql()); } - /** @testdox It should be possible to create a query using (LEFT) join for a relationship */ + /** + * @testdox It should be possible to create a query using (LEFT) join for a relationship + * @group join + */ public function testLeftJoin() { // Single Condition @@ -724,7 +736,10 @@ public function testLeftJoin() $this->assertValidSQL($builder->getQuery()->getRawSql()); } - /** @testdox It should be possible to create a query using (CROSS) join for a relationship */ + /** + * @testdox It should be possible to create a query using (CROSS) join for a relationship + * @group join + */ public function testCrossJoin() { // Single Condition @@ -738,7 +753,10 @@ public function testCrossJoin() $this->assertValidSQL($builder->getQuery()->getRawSql()); } - /** @testdox It should be possible to create a query using (INNER) join for a relationship */ + /** + * @testdox It should be possible to create a query using (INNER) join for a relationship + * @group join + */ public function testInnerJoin() { // Single Condition @@ -752,7 +770,10 @@ public function testInnerJoin() $this->assertValidSQL($builder->getQuery()->getRawSql()); } - /** @testdox It should be possible to create a conditional join using multiple ON with AND conditions */ + /** + * @testdox It should be possible to create a conditional join using multiple ON with AND conditions + * @group join + */ public function testMultipleJoinAndViaClosure() { $builder = $this->queryBuilderProvider('prefix_') @@ -767,7 +788,10 @@ public function testMultipleJoinAndViaClosure() $this->assertValidSQL($builder->getQuery()->getRawSql()); } - /** @testdox It should be possible to create a conditional join using multiple ON with OR conditions */ + /** + * @testdox It should be possible to create a conditional join using multiple ON with OR conditions + * @group join + */ public function testMultipleJoinOrViaClosure() { $builder = $this->queryBuilderProvider('prefix_') @@ -782,6 +806,17 @@ public function testMultipleJoinOrViaClosure() $this->assertValidSQL($builder->getQuery()->getRawSql()); } + /** + * @testdox It should be possible to do a join with a table as an alias + * @group join + */ + public function testJoinWithAlias(): void + { + $builder = $this->queryBuilderProvider('prefix_') + ->table('foo') + ->crossJoin(['bar' => 'foo'], 'bar.id', '=', 'foo.id'); + } + ################################################# ## SUB AND RAW QUERIES ## diff --git a/tests/Unit/Critiera/TestCritieraBuilder.php b/tests/Unit/Critiera/TestCritieraBuilder.php index 257f1cf..f6183c0 100644 --- a/tests/Unit/Critiera/TestCritieraBuilder.php +++ b/tests/Unit/Critiera/TestCritieraBuilder.php @@ -16,7 +16,7 @@ use Pixie\Connection; use Pixie\QueryBuilder\Raw; use Pixie\Tests\Logable_WPDB; -use Pixie\Criteria\CriteriaBuilder; +use Pixie\Parser\CriteriaBuilder; use Pixie\Statement\WhereStatement; /** @@ -54,6 +54,5 @@ public function testBuildWhereBetween(): void $statement1 = new WhereStatement('table.field2', 'BETWEEN', [Binding::asInt('123'), 787879], 'OR'); $builder = $this->getBuilder(['prefix' => 'ff_']); $builder->fromStatements([$statement,$statement1]); - // dump($builder->getCriteria()); } } From aab02b25d840552ab1cb858ef4e0544fb55a670c Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Sun, 27 Feb 2022 01:07:37 +0000 Subject: [PATCH 23/24] Clean up for join --- src/Parser/Normalizer.php | 3 -- src/Parser/StatementParser.php | 26 ++-------- src/QueryBuilder/WPDBAdapter.php | 63 ++++++++++++++++++----- tests/Unit/Parser/TestStatementParser.php | 30 ++++++++++- 4 files changed, 81 insertions(+), 41 deletions(-) diff --git a/src/Parser/Normalizer.php b/src/Parser/Normalizer.php index 22b0c9e..ae5fea6 100644 --- a/src/Parser/Normalizer.php +++ b/src/Parser/Normalizer.php @@ -32,14 +32,11 @@ use Pixie\QueryBuilder\Raw; use Pixie\JSON\JsonSelector; use Pixie\Parser\TablePrefixer; -use Pixie\Statement\JoinStatement; use Pixie\JSON\JsonSelectorHandler; -use Pixie\QueryBuilder\JoinBuilder; use Pixie\Statement\TableStatement; use Pixie\Statement\SelectStatement; use Pixie\JSON\JsonExpressionFactory; use Pixie\Statement\OrderByStatement; -use Pixie\QueryBuilder\JsonQueryBuilder; class Normalizer { diff --git a/src/Parser/StatementParser.php b/src/Parser/StatementParser.php index 43ba278..9ec54dc 100644 --- a/src/Parser/StatementParser.php +++ b/src/Parser/StatementParser.php @@ -59,33 +59,13 @@ class StatementParser /** @var Normalizer */ protected $normalizer; - public function __construct(Connection $connection) + public function __construct(Connection $connection, Normalizer $normalizer) { $this->connection = $connection; - $this->normalizer = $this->createNormalizer($connection); + $this->normalizer = $normalizer; } - /** - * Creates a full populated instance of the normalizer - * - * @param Connection $connection - * @return Normalizer - */ - private function createNormalizer($connection): Normalizer - { - // Create the table prefixer. - $adapterConfig = $connection->getAdapterConfig(); - $prefix = isset($adapterConfig[Connection::PREFIX]) - ? $adapterConfig[Connection::PREFIX] - : null; - - return new Normalizer( - new WpdbHandler($connection), - new TablePrefixer($prefix), - new JsonSelectorHandler(), - new JsonExpressionFactory($connection) - ); - } + /** * Normalizes and Parsers an array of SelectStatements. diff --git a/src/QueryBuilder/WPDBAdapter.php b/src/QueryBuilder/WPDBAdapter.php index f5e02f8..8ae89ef 100644 --- a/src/QueryBuilder/WPDBAdapter.php +++ b/src/QueryBuilder/WPDBAdapter.php @@ -12,10 +12,16 @@ use function is_float; +use Pixie\WpdbHandler; use Pixie\QueryBuilder\Raw; +use Pixie\Parser\Normalizer; +use Pixie\Statement\Statement; +use Pixie\Parser\TablePrefixer; use Pixie\Parser\CriteriaBuilder; use Pixie\Parser\StatementParser; +use Pixie\JSON\JsonSelectorHandler; use Pixie\Statement\SelectStatement; +use Pixie\JSON\JsonExpressionFactory; use Pixie\Statement\StatementBuilder; use Pixie\QueryBuilder\NestedCriteria; @@ -31,43 +37,72 @@ class WPDBAdapter */ protected $connection; + /** + * @var StatementParser + */ + protected $statementParser; + public function __construct(Connection $connection) { $this->connection = $connection; + $this->statementParser = new StatementParser($connection, $this->createNormalizer($connection)); + } + + /** + * Creates a full populated instance of the normalizer + * + * @param Connection $connection + * @return Normalizer + */ + private function createNormalizer($connection): Normalizer + { + // Create the table prefixer. + $adapterConfig = $connection->getAdapterConfig(); + $prefix = isset($adapterConfig[Connection::PREFIX]) + ? $adapterConfig[Connection::PREFIX] + : null; + + return new Normalizer( + new WpdbHandler($connection), + new TablePrefixer($prefix), + new JsonSelectorHandler(), + new JsonExpressionFactory($connection) + ); } /** This is a mock for the new parser based select method. */ public function selectCol(StatementBuilder $col, $data, $statements) // @phpstan-ignore-line { - if (!array_key_exists('tables', $statements)) { + if (! $col->has(Statement::TABLE)) { throw new Exception('No table specified.', 3); - } elseif (!array_key_exists('selects', $statements)) { - $statements['selects'][] = '*'; } + // if (!array_key_exists('tables', $statements)) { + // } elseif (!array_key_exists('selects', $statements)) { + // $statements['selects'][] = '*'; + // } - $parser = new StatementParser($this->connection); if (!$col->hasSelect()) { $col->addSelect(new SelectStatement('*')); } - $where = $parser->parseWhere($col->getWhere()); - $having = $parser->parseHaving($col->getHaving()); - $join = $parser->parseJoin($col->getJoin()); + $where = $this->statementParser->parseWhere($col->getWhere()); + $having = $this->statementParser->parseHaving($col->getHaving()); + $join = $this->statementParser->parseJoin($col->getJoin()); /** @var string[] */ $sqlArray = [ 'SELECT' . ($col->getDistinctSelect() ? ' DISTINCT' : ''), - $parser->parseSelect($col->getSelect()), + $this->statementParser->parseSelect($col->getSelect()), 'FROM', - $parser->parseTable($col->getTable()), - $parser->parseJoin($col->getJoin()), + $this->statementParser->parseTable($col->getTable()), + $this->statementParser->parseJoin($col->getJoin()), $where->getStatement(), - $parser->parseGroupBy($col->getGroupBy()), + $this->statementParser->parseGroupBy($col->getGroupBy()), $having->getStatement(), - $parser->parseOrderBy($col->getOrderBy()), - $parser->parseLimit($col->getLimit()), - $parser->parseOffset($col->getOffset()), + $this->statementParser->parseOrderBy($col->getOrderBy()), + $this->statementParser->parseLimit($col->getLimit()), + $this->statementParser->parseOffset($col->getOffset()), ]; $sql = $this->concatenateQuery($sqlArray); diff --git a/tests/Unit/Parser/TestStatementParser.php b/tests/Unit/Parser/TestStatementParser.php index 17ec06b..0b7f2bc 100644 --- a/tests/Unit/Parser/TestStatementParser.php +++ b/tests/Unit/Parser/TestStatementParser.php @@ -15,14 +15,19 @@ use TypeError; use WP_UnitTestCase; use Pixie\Connection; +use Pixie\WpdbHandler; use Pixie\QueryBuilder\Raw; use Pixie\JSON\JsonSelector; +use Pixie\Parser\Normalizer; use Pixie\Tests\Logable_WPDB; use Pixie\Statement\Statement; +use Pixie\Parser\TablePrefixer; use Pixie\Parser\StatementParser; +use Pixie\JSON\JsonSelectorHandler; use Pixie\Statement\TableStatement; use Pixie\Tests\SQLAssertionsTrait; use Pixie\Statement\SelectStatement; +use Pixie\JSON\JsonExpressionFactory; use Pixie\Statement\StatementBuilder; /** @@ -53,7 +58,30 @@ public function setUp(): void */ public function getParser(array $connectionConfig = []): StatementParser { - return new StatementParser(new Connection($this->wpdb, $connectionConfig)); + $connection = new Connection($this->wpdb, $connectionConfig); + return new StatementParser($connection, $this->createNormalizer($connection)); + } + + /** + * Creates a full populated instance of the normalizer + * + * @param Connection $connection + * @return Normalizer + */ + private function createNormalizer($connection): Normalizer + { + // Create the table prefixer. + $adapterConfig = $connection->getAdapterConfig(); + $prefix = isset($adapterConfig[Connection::PREFIX]) + ? $adapterConfig[Connection::PREFIX] + : null; + + return new Normalizer( + new WpdbHandler($connection), + new TablePrefixer($prefix), + new JsonSelectorHandler(), + new JsonExpressionFactory($connection) + ); } /** @testdox Is should be possible to parse all expected select values from string, json arrow and selector objects and raw expressions and have them returned as a valid SQL fragment. */ From cb5e75d0c69d3679ce103d4bd8554c7029d2ba72 Mon Sep 17 00:00:00 2001 From: Glynn Quelch Date: Fri, 15 Apr 2022 11:06:26 +0100 Subject: [PATCH 24/24] Stashed --- src/Parser/StatementParser.php | 20 +++ src/QueryBuilder/QueryBuilderHandler.php | 6 + src/QueryBuilder/WPDBAdapter.php | 29 +++- src/Statement/InsertStatement.php | 136 ++++++++++++++++++ src/Statement/Statement.php | 1 + src/Statement/StatementBuilder.php | 19 ++- .../TestIntegrationWithWPDB.php | 6 +- 7 files changed, 210 insertions(+), 7 deletions(-) create mode 100644 src/Statement/InsertStatement.php diff --git a/src/Parser/StatementParser.php b/src/Parser/StatementParser.php index 9ec54dc..33f43d9 100644 --- a/src/Parser/StatementParser.php +++ b/src/Parser/StatementParser.php @@ -30,6 +30,7 @@ use Pixie\WpdbHandler; use Pixie\Parser\Criteria; use Pixie\Parser\Normalizer; +use Pixie\Statement\Statement; use Pixie\Parser\CriteriaBuilder; use Pixie\Statement\JoinStatement; use Pixie\JSON\JsonSelectorHandler; @@ -41,6 +42,7 @@ use Pixie\JSON\JsonExpressionFactory; use Pixie\Statement\GroupByStatement; use Pixie\Statement\OrderByStatement; +use Pixie\Statement\StatementBuilder; class StatementParser { @@ -92,6 +94,17 @@ public function parseSelect(array $select): string return join(', ', $select); } + public function table(StatementBuilder $builder, bool $single = false): string + { + if (!$builder->has(Statement::TABLE)) { + return ''; + } + + return true === $single + ? $this->parseTable([current($builder->getTable())]) + : $this->parseTable($builder->getTable()); + } + /** * Normalizes and Parsers an array of TableStatements * @@ -262,4 +275,11 @@ public function parseJoin(array $join): string return join(' ', $joins); } + + public function parseInsert(StatementBuilder $builder): string + { + $statement = $builder->getInsert(); + dump($statement); + return ''; + } } diff --git a/src/QueryBuilder/QueryBuilderHandler.php b/src/QueryBuilder/QueryBuilderHandler.php index 80416f3..386186c 100644 --- a/src/QueryBuilder/QueryBuilderHandler.php +++ b/src/QueryBuilder/QueryBuilderHandler.php @@ -22,6 +22,7 @@ use Pixie\Statement\TableStatement; use Pixie\Statement\WhereStatement; use Pixie\Statement\HavingStatement; +use Pixie\Statement\InsertStatement; use Pixie\Statement\SelectStatement; use Pixie\QueryBuilder\TablePrefixer; use Pixie\Statement\GroupByStatement; @@ -571,6 +572,11 @@ private function doInsert(array $data, string $type) return $eventResult; } + $statement = new InsertStatement($data, $type); + $this->statementBuilder->addStatement($statement); + $q = $this->adapterInstance->doInsertB($this->statementBuilder); + dump($q); + // If first value is not an array () not a batch insert) if (! is_array(current($data))) { $queryObject = $this->getQuery($type, $data); diff --git a/src/QueryBuilder/WPDBAdapter.php b/src/QueryBuilder/WPDBAdapter.php index 8ae89ef..108ac32 100644 --- a/src/QueryBuilder/WPDBAdapter.php +++ b/src/QueryBuilder/WPDBAdapter.php @@ -77,12 +77,8 @@ public function selectCol(StatementBuilder $col, $data, $statements) // @phpstan if (! $col->has(Statement::TABLE)) { throw new Exception('No table specified.', 3); } - // if (!array_key_exists('tables', $statements)) { - // } elseif (!array_key_exists('selects', $statements)) { - // $statements['selects'][] = '*'; - // } - if (!$col->hasSelect()) { + if (!$col->has(Statement::SELECT)) { $col->addSelect(new SelectStatement('*')); } @@ -216,6 +212,29 @@ public function criteriaOnly(array $statements, bool $bindValues = true): array return compact('sql', 'bindings'); } + /** + * Build a generic insert/ignore/replace query + * + * @param array $statements + * @param array $data + * @param string $type + * + * @return array{sql:string, bindings:mixed[]} + * + * @throws Exception + */ + public function doInsertB(StatementBuilder $col): array + { + $tables = $this->statementParser->table($col, true); + + $sql = ''; + $bindings = []; + + dump($col, $tables); + + return compact('sql', 'bindings'); + } + /** * Build a generic insert/ignore/replace query * diff --git a/src/Statement/InsertStatement.php b/src/Statement/InsertStatement.php new file mode 100644 index 0000000..fe71bac --- /dev/null +++ b/src/Statement/InsertStatement.php @@ -0,0 +1,136 @@ + + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @package Gin0115\Pixie + * @subpackage QueryBuilder\Statement + */ + +namespace Pixie\Statement; + +use TypeError; +use Pixie\Binding; +use Pixie\QueryBuilder\Raw; +use Pixie\Statement\Statement; + +class InsertStatement implements Statement +{ + + protected const IGNORE = 'INSERT IGNORE'; + protected const REPLACE = 'REPLACE'; + + /** + * The data to inserted or updated. + * + * @var array> + */ + protected $data = []; + + /** + * Holds the insert type + * + * @var string + */ + protected $type; + + public function __construct(array $data, string $type = 'INSERT') + { + $this->data = $this->normalizeData($data); + $this->type = $type; + } + + /** + * Get the statement type + * + * @return string + */ + public function getType(): string + { + return Statement::INSERT; + } + + /** + * Undocumented function + * + * @param mixed[] $data + * @return array> + */ + protected function normalizeData(array $data): array + { + // If single array. + if (!is_array(current($data))) { + $data = [$data]; + } + + return array_reduce($data, function (array $carry, $row): array { + foreach ($row as $key => $value) { + $this->verifyKey($key); + $this->verifyValue($value); + } + $carry[] = $row; + return $carry; + }, []); + } + + /** + * Verifies if the passed filed is of a valid type. + * + * @param mixed $value + * @return void + */ + protected function verifyValue($value): void + { + if ( + !is_string($value) + && !is_int($value) + && !is_float($value) + && !is_bool($value) + && !is_null($value) + && !is_a($value, Raw::class) + && !is_a($value, Binding::class) + ) { + throw new TypeError( + sprintf( + "Only Binding, Raw, string, float, int, bool, null accepted as values for insert, %s passed", + json_encode($value) + ) + ); + } + } + + /** + * Verifies that only strings can be used as keys. + * + * @param [type] $key + * @return void + */ + protected function verifyKey($key): void + { + if (!is_string($key)) { + throw new TypeError( + sprintf( + "Only strings accepted as keys for insert, %s passed", + json_encode($key) + ) + ); + } + } +} diff --git a/src/Statement/Statement.php b/src/Statement/Statement.php index f25ad46..59b720b 100644 --- a/src/Statement/Statement.php +++ b/src/Statement/Statement.php @@ -38,6 +38,7 @@ interface Statement public const WHERE = 'where'; public const HAVING = 'having'; public const JOIN = 'join'; + public const INSERT = 'insert'; /** * Get the statement type diff --git a/src/Statement/StatementBuilder.php b/src/Statement/StatementBuilder.php index 63264f0..8dd7fff 100644 --- a/src/Statement/StatementBuilder.php +++ b/src/Statement/StatementBuilder.php @@ -166,7 +166,17 @@ public function getTable(): array */ public function hasTable(): bool { - return 0 < count($this->getTable()); + return 0 < $this->countTable(); + } + + /** + * Counts the number of table statements. + * + * @return int + */ + public function countTable(): int + { + return count($this->getTable()); } /** @@ -384,4 +394,11 @@ public function setOffset(?int $offset): self $this->offset = $offset; return $this; } + + public function getInsert(): ?InsertStatement + { + return $this->has(Statement::INSERT) + ? current($this->statements[Statement::INSERT]) + : null; + } } diff --git a/tests/QueryBuilderHandler/TestIntegrationWithWPDB.php b/tests/QueryBuilderHandler/TestIntegrationWithWPDB.php index b06d109..36f5e1e 100644 --- a/tests/QueryBuilderHandler/TestIntegrationWithWPDB.php +++ b/tests/QueryBuilderHandler/TestIntegrationWithWPDB.php @@ -348,7 +348,11 @@ public function testJoins(): void $this->assertEmpty($this->arrayDifMD($expected, $fruitsRight)); } - /** @testdox It should be possible to INSERT a single or multiple tables and return the primary key. It should then possible to get the same values back using find() */ + /** + * @testdox It should be possible to INSERT a single or multiple tables and return the primary key. It should then possible to get the same values back using find() + * @group insert + * @group find + * */ public function testInsertAndFind(): void { $this->queryBuilderProvider('mock_', 'BB');