diff --git a/CHANGELOG.md b/CHANGELOG.md index 206b83e..f2b6c4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## v1.0.2 (2024-04-23) + +- QueryBuilder::quoteIdentifier() +- QueryBuilder fromArray is not longer static +- QueryBuilder\Query - removed static fromArray methods (except in Conditions) +- Updated docs + ## v1.0.1 (2024-04-21) - QueryBuilder::join() added diff --git a/docs/api/conditions.md b/docs/api/conditions.md index 88bb352..65d920c 100644 --- a/docs/api/conditions.md +++ b/docs/api/conditions.md @@ -80,6 +80,8 @@ There are many predefined operators (methods) available: - gte(): Greater than or equal `>=` - isNull(): `IS NULL` - isNotNull(): `IS NOT NULL` +- like(): `LIKE` +- notLike(): `NOT LIKE` !!! note More should be added (LIKE, NOT LIKE, IN, NOT IN, ...) diff --git a/docs/api/querybuilder.md b/docs/api/querybuilder.md index 20209e0..253ad56 100644 --- a/docs/api/querybuilder.md +++ b/docs/api/querybuilder.md @@ -2,22 +2,25 @@ Query builder is a heart of every DBAL. It strives to support any SQL query. -## SELECT query +## Queries + +### SELECT query Select query is started with issuing `select()` method. ```php select('column as A', 'column AS B'); +$queryBuilder->select('columnA', 'columnB'); ``` SELECT queries are built from: -- [SELECT](#select-query) expression -- [FROM](#from-clause) expression +- [SELECT](#select-query) clause +- [FROM](#from-clause) clause +- [JOIN](#join-clause) clause - [WHERE](#where) [conditions](conditions.md) -- [ORDER BY](#order-by) expression -- [LIMIT](#limit) expression +- [ORDER BY](#order-by) clause +- [LIMIT](#limit) clause **Example** @@ -26,6 +29,7 @@ SELECT queries are built from: $queryBuilder ->select('column') ->from('table') + ->join('another', 'table.another_id = another.id', 'another') ->where($queryBuilder->condition('column', '=', '?')) ->orderBy('column', 'ASC') ->limit(1) @@ -35,7 +39,7 @@ $queryBuilder !!! note See [Conditions](conditions.md) on how to construct `WHERE` conditions. -## UPDATE query +### UPDATE query Update query is started by issuing `update()` method. @@ -47,10 +51,11 @@ $queryBuilder->update('table', 'alias'); UPDATE queries are built from: - [FROM](#from-clause) table to update -- [SET](#set) expression -- [WHERE](#where) [conditions](conditions.md) -- [ORDER BY](#order-by) expression -- [LIMIT](#limit) expression +- [JOIN](#join-clause) clause +- [SET](#valuesset-clause) clause +- [WHERE](#where-clause) [conditions](conditions.md) +- [ORDER BY](#order-by) clause +- [LIMIT](#limit) clause **Example** @@ -62,13 +67,13 @@ $queryBuilder 'columnA' => 'valueA', 'columnB' => 'valueB' ]) - ->where($queryBuilder->condition('column', '=', '?')) - ->orderBy('column', 'ASC') + ->where($queryBuilder->condition('columnC', '=', '?')) + ->orderBy('columnD', 'ASC') ->limit(1) ->setParameters('condition'); ``` -## INSERT query +### INSERT query Insert query is started by issuing `insert()` method. @@ -80,8 +85,8 @@ $queryBuilder->insert('table', 'alias'); INSERT queries are built from: - [INTO](#into-clause) table to insert into -- [COLUMNS](#values) columns definition -- [VALUES](#values) values +- [COLUMNS](#valuesset-clause) clause +- [VALUES](#valuesset-clause) clause **Example** @@ -95,7 +100,7 @@ $queryBuilder ]); ``` -## DELETE query +### DELETE query Delete query is started by issuing `delete()` method. @@ -107,7 +112,7 @@ $queryBuilder->delete('table', 'alias'); DELETE queries are built from: - [FROM](#from-clause) table to delete from -- [WHERE](#where) [conditions](conditions.md) +- [WHERE](#where-clause) [conditions](conditions.md) - [ORDER BY](#order-by) expressions - [LIMIT](#limit) expressions @@ -123,7 +128,9 @@ $queryBuilder ->setParameters('condition'); ``` -## FROM clause +## Clauses + +### FROM clause ```php create()->then( )->then( function (Blrf\Dbal\Result $result) { print_r($result->rows); + // or you can iterate result rows directly + foreach ($result as $row) { + print_r($row); + } } ); ``` diff --git a/examples/select.php b/examples/select.php index ec4f668..c3f6175 100644 --- a/examples/select.php +++ b/examples/select.php @@ -28,5 +28,9 @@ function (Blrf\Dbal\Connection $db) { )->then( function (Blrf\Dbal\Result $result) { print_r($result->rows); + // or + foreach ($result as $row) { + print_r($row); + } } ); diff --git a/examples/update.php b/examples/update.php new file mode 100644 index 0000000..0d26d44 --- /dev/null +++ b/examples/update.php @@ -0,0 +1,23 @@ +create()->then( + function (Blrf\Dbal\Connection $db) { + // start query builder + $qb = $db->query() + ->update('book') + ->values(['title' => 'Moby Dick: ' . time()]); // automatically added to parameters + $qb->where($qb->condition('book_id')) + ->addParameter([11089]) // we add parameter + ->limit(1); + echo "sql: " . $qb->getSql() . "\n"; + // sql: UPDATE book SET title = ? WHERE book_id = ? LIMIT 1 + return $qb->execute(); + } +)->then( + function (Blrf\Dbal\Result $result) { + print_r($result); + } +); diff --git a/examples/updateJoin.php b/examples/updateJoin.php new file mode 100644 index 0000000..ab5111a --- /dev/null +++ b/examples/updateJoin.php @@ -0,0 +1,38 @@ +create()->then( + function (Blrf\Dbal\Connection $db) { + // start query builder + $qb = $db->query() + ->update('address') + ->join( + 'customer_address', + 'customer_address.address_id = address.address_id' + ) + ->join( + 'address_status', + 'address_status.status_id = customer_address.status_id' + ) + ->values(['address.street_name' => 'Inactive: ' . time()]) // parameter is added + ->where( + fn($cb) => $cb->and( + $cb->eq('address_status.address_status'), + $cb->eq('customer_address.customer_id') + ) + ) + ->addParameter('Inactive', 3); + echo "sql: " . $qb->getSql() . "\n"; + // sql: UPDATE address INNER JOIN customer_address ON customer_address.address_id = address.address_id + // INNER JOIN address_status ON address_status.status_id = customer_address.status_id + // SET address.street_name = ? + // WHERE (address_status.address_status = ? AND customer_address.customer_id = ?) + return $qb->execute(); + } +)->then( + function (Blrf\Dbal\Result $result) { + print_r($result); + } +); diff --git a/src/Query/Condition.php b/src/Query/Condition.php index 5aebb6c..c08eb23 100644 --- a/src/Query/Condition.php +++ b/src/Query/Condition.php @@ -9,6 +9,18 @@ /** * Single condition for WHERE or HAVING + * + * @phpstan-type ConditionToArray array{ + * expression: string, + * operator: string, + * value: mixed + * } + * + * @phpstan-type ConditionFromArray array{ + * expression: string, + * operator: string, + * value: mixed + * } */ class Condition implements Stringable { @@ -17,10 +29,6 @@ class Condition implements Stringable 'is null', 'is not null' ]; - /** - * Value will be null if operator is in noValueOperators - */ - public readonly ?string $value; /** * Create condition from array * @@ -71,6 +79,11 @@ public static function fromArray(array $data): static|ConditionGroup return new static($expression, $operator, $value); } + /** + * Value will be null if operator is in noValueOperators + */ + public readonly ?string $value; + final public function __construct( public readonly string $expression, public readonly string $operator = '=', @@ -87,7 +100,7 @@ public function __toString(): string return $this->expression . ' ' . $this->operator . ($this->value === null ? '' : ' ' . $this->value); } - /** @return array{expression:string, operator:string, value:string|null} */ + /** @return ConditionToArray */ public function toArray(): array { return [ diff --git a/src/Query/ConditionGroup.php b/src/Query/ConditionGroup.php index 8230145..ada1fd0 100644 --- a/src/Query/ConditionGroup.php +++ b/src/Query/ConditionGroup.php @@ -11,6 +11,10 @@ /** * Group of conditions + * + * @phpstan-type ConditionGroupToArray non-empty-array<'AND'|'OR', array> + * + * @phpstan-type ConditionGroupFromArray array{'OR': array>}|array{'AND': array>} */ class ConditionGroup implements Stringable { @@ -26,7 +30,7 @@ class ConditionGroup implements Stringable * 'type' => [ condition, ... ] * ] * - * @param array{'OR': array>}|array{'AND': array>} $data + * @param ConditionGroupFromArray $data */ public static function fromArray(array $data): static { @@ -67,7 +71,7 @@ public function __toString(): string * ] * ``` * - * @return non-empty-array<'AND'|'OR', array> + * @return ConditionGroupToArray */ public function toArray(): array { diff --git a/src/Query/Expression.php b/src/Query/Expression.php index 5b09b80..9b440d0 100644 --- a/src/Query/Expression.php +++ b/src/Query/Expression.php @@ -8,12 +8,6 @@ abstract class Expression implements Stringable { - /** - * @param array $data - */ - abstract public static function fromArray(array $data): static; - - abstract public static function fromString(string $from): static; /** @return array */ abstract public function toArray(): array; diff --git a/src/Query/FromExpression.php b/src/Query/FromExpression.php index 1c8ab3b..41f1c8c 100644 --- a/src/Query/FromExpression.php +++ b/src/Query/FromExpression.php @@ -12,55 +12,19 @@ /** * FROM [expression] + * + * @phpstan-type FromFromArray array{ + * expression: string|array, + * alias?: string|null + * } + * + * @phpstan-type FromToArray array{ + * expression: string|array, + * alias: string|null + * } */ class FromExpression extends Expression { - /** - * Create FromExpression from array - * - * Data keys: - * - * - expression - * - string - * - array with key 'class' will call class::fromArray() which is expected to be a QueryBuilderInterface - * - alias - * - * @param array{expression?:string|array, alias?:string} $data - */ - public static function fromArray(array $data): static - { - $expression = $data['expression'] ?? ''; - /** - * Is it a subquery? - */ - if (is_array($expression) && isset($expression['class'])) { - $class = $expression['class']; - $expression = $class::fromArray($expression); - } - $alias = $data['alias'] ?? null; - return new static($expression, $alias); - } - - /** - * Create from expression from string - * - * @note Currently does not support subquery as QueryBuilder - * But could probably be done with `(...) AS x` match. - * Very basic regexp. - */ - public static function fromString(string $from): static - { - $expression = ''; - $alias = null; - if (preg_match('/(.*)( AS )+(.*)/iu', $from, $matches)) { - $expression = $matches[1]; - $alias = $matches[3]; - } else { - $expression = $from; - } - return new static($expression, $alias); - } - /** * From expression * @@ -73,6 +37,9 @@ final public function __construct( if (is_string($expression) && strlen($expression) == 0) { throw new ValueError('Expression cannot be empty'); } + if ($expression instanceof QueryBuilderInterface && empty($alias)) { + throw new ValueError('Expression is QueryBuilder with empty alias'); + } } public function __toString(): string @@ -83,7 +50,7 @@ public function __toString(): string return $this->expression . ($this->alias === null ? '' : ' AS ' . $this->alias); } - /** @return array{expression:string|array, alias: string|null} */ + /** @return FromToArray */ public function toArray(): array { return [ diff --git a/src/Query/JoinExpression.php b/src/Query/JoinExpression.php index 53cd326..33aba53 100644 --- a/src/Query/JoinExpression.php +++ b/src/Query/JoinExpression.php @@ -7,34 +7,26 @@ use ValueError; /** + * JOIN [expression] + * * @phpstan-type JoinFromArray array{ * table: string, * on: string, * alias?: string|null, * type?: string|JoinType * } + * + * @phpstan-type JoinToArray array{ + * table: string, + * on: string, + * alias: string|null, + * type: string + * } */ class JoinExpression extends Expression { public readonly JoinType $type; - /** - * @param JoinFromArray $data - */ - public static function fromArray(array $data): static - { - $table = $data['table'] ?? ''; - $on = $data['on'] ?? ''; - $alias = $data['alias'] ?? null; - $type = $data['type'] ?? JoinType::INNER; - return new static($type, $table, $on, $alias); - } - - public static function fromString(string $join): static - { - throw new \Exception('Not implemented'); - } - final public function __construct( JoinType|string $type, public readonly string $table, @@ -60,13 +52,7 @@ public function __toString(): string ' ON ' . $this->on; } - /** @return array{ - * table: string, - * on: string, - * alias: string|null, - * type: string - * } - */ + /** @return JoinToArray **/ public function toArray(): array { return [ diff --git a/src/Query/Limit.php b/src/Query/Limit.php index ad6c860..182fcdb 100644 --- a/src/Query/Limit.php +++ b/src/Query/Limit.php @@ -12,50 +12,22 @@ * * MySQL: You can not really use offset without limit. If you need it, set offset and limit to PHP_INT_MAX. * PostgreSql: Limit may be ALL + * + * @phpstan-type LimitFromArray array{ + * limit?: int|null, + * offset?: int|null + * } + * + * @phpstan-type LimitToArray array{ + * limit: int|null, + * offset: int|null + * } */ class Limit extends Expression { protected ?int $limit = null; protected ?int $offset = null; - /** - * Limit from array - * - * Data keys: - * - * - limit - * - offset - * - * @param array{limit?:int|null, offset?:int|null} $data - */ - public static function fromArray(array $data): static - { - $limit = isset($data['limit']) ? (int)$data['limit'] : null; - $offset = isset($data['offset']) ? (int)$data['offset'] : null; - return new static($limit, $offset); - } - - /** - * Limit from string - * - * Currently supports string: - * - * - LIMIT l OFFSET o - */ - public static function fromString(string $data): static - { - $limit = null; - $offset = null; - preg_match('/LIMIT\s+(\d+)(\s+OFFSET\s+(\d+))?/i', $data, $matches); - if (isset($matches[1])) { - $limit = (int)$matches[1]; - } - if (isset($matches[3])) { - $offset = (int)$matches[3]; - } - return new static($limit, $offset); - } - /** * Construct new Limit clause * @@ -87,7 +59,7 @@ public function __toString(): string return $ret; } - /** @return array{limit: int|null, offset: int|null} */ + /** @return LimitToArray */ public function toArray(): array { return ['limit' => $this->limit, 'offset' => $this->offset]; diff --git a/src/Query/OrderByExpression.php b/src/Query/OrderByExpression.php index 53ae639..f75933d 100644 --- a/src/Query/OrderByExpression.php +++ b/src/Query/OrderByExpression.php @@ -10,37 +10,20 @@ /** * ORDER BY [expression] + * + * @phpstan-type OrderByFromArray array{ + * expression: string, + * type?: string|OrderByType + * } + * + * @phpstan-type OrderByToArray array{ + * expression: string, + * type: string + * } */ class OrderByExpression extends Expression { public readonly OrderByType $type; - /** - * Create order by expression from array - * - * Data keys: - * - * - expression: string - * - type: ?string (OrderByType) - * - * @param array{expression?: string, type?:OrderByType|string} $data - */ - public static function fromArray(array $data): static - { - return new static($data['expression'] ?? '', $data['type'] ?? OrderByType::ASC); - } - - /** - * Create select expression from string - * - * @note regexp is very basic - */ - public static function fromString(string $string): static - { - preg_match('/([^\s]+)\s?(ASC|DESC)?/ui', $string, $matches); - $expression = $matches[1]; - $type = strtoupper($matches[2] ?? 'ASC'); - return new static($expression, $type); - } final public function __construct( public readonly string $expression, @@ -60,9 +43,12 @@ public function __toString(): string return $this->expression . ' ' . $this->type->value; } - /** @return array{expression: string, type: string} */ + /** @return OrderByToArray */ public function toArray(): array { - return ['expression' => $this->expression, 'type' => $this->type->value]; + return [ + 'expression' => $this->expression, + 'type' => $this->type->value + ]; } } diff --git a/src/Query/SelectExpression.php b/src/Query/SelectExpression.php index e2ee5cc..584f3e0 100644 --- a/src/Query/SelectExpression.php +++ b/src/Query/SelectExpression.php @@ -9,42 +9,19 @@ /** * SELECT [expression] + * + * @phpstan-type SelectFromArray array{ + * expression: string, + * alias?: string|null + * } + * + * @phpstan-type SelectToArray array{ + * expression: string, + * alias: string|null + * } */ class SelectExpression extends Expression { - /** - * Create select expression from array - * - * Data keys: - * - * - expression: string - * - alias: ?string - * - * @param array{expression?:string, alias?:string} $data - */ - public static function fromArray(array $data): static - { - return new static($data['expression'] ?? '', $data['alias'] ?? null); - } - - /** - * Create select expression from string - * - * @note regexp is very basic - */ - public static function fromString(string $string): static - { - $expression = ''; - $alias = null; - if (preg_match('/(.+?)\W+AS\W+(.*)/iu', $string, $matches)) { - $expression = trim($matches[1]); - $alias = trim($matches[2]); - } else { - $expression = $string; - } - return new static($expression, $alias); - } - /** * Create select expression * @@ -67,7 +44,7 @@ public function __toString(): string return $ret; } - /** @return array{expression: string, alias: string|null} */ + /** @return SelectToArray */ public function toArray(): array { return [ diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 1a29a3a..ba766cc 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -17,6 +17,7 @@ use Blrf\Dbal\Query\Type; use Blrf\Dbal\Driver\QueryBuilder as DriverQueryBuilder; use TypeError; +use ValueError; use array_map; use is_array; use is_string; @@ -30,7 +31,10 @@ * * NOTE: It will not validate queries. * - * @phpstan-import-type JoinFromArray from JoinExpression + * @phpstan-import-type QueryBuilderFromArray from QueryBuilderInterface + * @phpstan-import-type QueryBuilderToArray from QueryBuilderInterface + * + * @phpstan-import-type SelectFromArray from SelectExpression */ class QueryBuilder implements QueryBuilderInterface { @@ -55,212 +59,164 @@ class QueryBuilder implements QueryBuilderInterface * Joins * @var array */ - protected array $joins = []; + protected array $join = []; + /** + * Where conditions + */ protected Condition|ConditionGroup|null $where = null; /** * Order by expressions * @var array */ protected array $orderBy = []; + /** + * Limit + */ protected ?Limit $limit = null; /** * Parameters * @var array */ protected array $parameters = []; - /** - * Create query builder from array - * - * # Array keys - * - * ## `class` string - * - * Which class to use to construct query builder (defaults to static class) - * - * ## `type` string - * - * Type of query (default: SELECT) - * - * ## `select` string|array - * - * Select expressions - * - * String: EXPRESSION AS ALIAS, EXPRESSION AS ALIAS, ... - * Array: [['expression' => 'EXPRESSION', 'alias' => 'ALIAS'], ...] - * - * ## `from` string|array|QueryBuilder - * - * From expressions + * Quote char * - * String: NA; TBD! - * Array: [['expression' => 'EXPRESSION'], 'alias' => 'ALIAS'] - * Array with subquery: [['expression' => QueryBuilder::toArray(), 'alias' => 'ALIAS']] - * - * ## `columns` array - * - * List of `INSERT` or `UPDATE` columns - * - * ## `where` Condition|ConditionGroup - * - * Simple condition: ['ex', 'op', 'value'] or - * ['expression' => 'ex', 'operator' => 'op', 'value' => 'value'] - * - * Group condition: ['TYPE' => [... conditions ... ] ] - * - * - * ## `orderBy` array - * - * List of `ORDER BY` columns - * - * ## `limit` array - * - * String: 'LIMIT l OFFSET o' - * Array: ['limit' => limit, 'offset' => offset] or $data['limit'], $data['offset'] - * - * ## `parameters` array - * - * List of parameters + * When you want to quote identifier use self::quoteIdentifier() method. + * @note DBAL will not quote identifiers + */ + protected string $quoteChar = '`'; + + /** + * Build query from array * - * @param array{ - * class?: string, - * type?: string|Type, - * select?: array|string, - * from?: array|string, - * join?: array, - * columns?: array, - * where?:array|null, - * values?:array, - * orderBy?:array|string, - * limit?:array{limit?: int|null, offset?: int|null}|int|null, - * offset?:int - * } $data - * @param mixed $arguments Arguments to QueryBuilder constructor - * @deprecated Most probably - */ - public static function fromArray(array $data, mixed ...$arguments): QueryBuilder|DriverQueryBuilder - { - $class = $data['class'] ?? static::class; - $qb = new $class(...$arguments); - if (!($qb instanceof QueryBuilder)) { - throw new TypeError('Provided class is not instance of QueryBuilder'); - } - if (isset($data['type'])) { - $qb->setType($data['type']); + * @param QueryBuilderFromArray $data + */ + public function fromArray(array $data): static + { + if (!isset($data['type'])) { + throw new ValueError('Missing type'); } + $this->setType($data['type']); + /** - * SELECT expressions + * Select expressions */ - if (isset($data['select'])) { - if (is_array($data['select'])) { - foreach ($data['select'] as $select) { - if (is_array($select)) { - $qb->addSelectExpression(SelectExpression::fromArray($select)); - } - if (is_string($select)) { - $qb->addSelectExpression(SelectExpression::fromString($select)); - } - } - } - if (is_string($data['select'])) { - foreach (explode(',', $data['select']) as $select) { - $qb->addSelectExpression(SelectExpression::fromString($select)); - } + if (isset($data['select']) && is_array($data['select'])) { + foreach ($data['select'] as $expr) { + $this->select(...$data['select']); } } + /** - * FROM expressions - * @note string types will cause problem with subqueries + * From expressions */ - if (isset($data['from'])) { - if (is_array($data['from'])) { - foreach ($data['from'] as $from) { - if (is_array($from)) { - $qb->addFromExpression(FromExpression::fromArray($from)); - } elseif (is_string($from)) { - $qb->addFromExpression(FromExpression::fromString($from)); + if (isset($data['from']) && is_array($data['from'])) { + foreach ($data['from'] as $fromExpr) { + if (is_array($fromExpr)) { + $expr = $fromExpr['expression'] ?? ''; + if (is_array($expr)) { + $qb = clone $this; + // @phpstan-ignore-next-line + $qb->fromArray($expr); + $expr = $qb; } + $this->addFromExpression( + $this->createFromExpression($expr, $fromExpr['alias'] ?? null) + ); + } elseif ($fromExpr instanceof FromExpression) { + $this->addFromExpression($fromExpr); } - } elseif (is_string($data['from'])) { - $qb->addFromExpression(FromExpression::fromString($data['from'])); } } /** - * JOIN expressions + * Join expressions */ if (isset($data['join']) && is_array($data['join'])) { - foreach ($data['join'] as $join) { - if (is_array($join)) { - $qb->addJoinExpression(JoinExpression::fromArray($join)); + foreach ($data['join'] as $joinExpr) { + if (is_array($joinExpr)) { + $this->addJoinExpression( + $this->createJoinExpression( + $joinExpr['type'] ?? JoinType::INNER, + $joinExpr['table'] ?? '', + $joinExpr['on'] ?? '', + $joinExpr['alias'] ?? null, + ) + ); + } elseif ($joinExpr instanceof JoinExpression) { + $this->addJoinExpression($joinExpr); } } } - $qb->columns = $data['columns'] ?? []; + + /** + * Columns + */ + if (isset($data['columns']) && is_array($data['columns'])) { + $this->columns = $data['columns']; + } + + /** + * Where conditions + */ if (isset($data['where']) && is_array($data['where'])) { - $qb->where = Condition::fromArray($data['where']); + $this->where = Condition::fromArray($data['where']); } /** - * orderBy expressions + * Order by expressions */ - if (isset($data['orderBy'])) { - if (is_array($data['orderBy'])) { - foreach ($data['orderBy'] as $orderBy) { - if (is_array($orderBy)) { - $qb->addOrderByExpression(OrderByExpression::fromArray($orderBy)); - } elseif (is_string($orderBy)) { - $qb->addOrderbyExpression(OrderByExpression::fromString($orderBy)); - } + if (isset($data['orderBy']) && is_array($data['orderBy'])) { + foreach ($data['orderBy'] as $orderByExpr) { + if (is_array($orderByExpr)) { + $this->addOrderByExpression( + $this->createOrderByExpression( + $orderByExpr['expression'] ?? '', + $orderByExpr['type'] ?? 'ASC' + ) + ); + } elseif ($orderByExpr instanceof OrderByExpression) { + $this->addOrderByExpression($orderByExpr); } - } elseif (is_string($data['orderBy'])) { - $qb->addOrderByExpression(OrderByExpression::fromString($data['orderBy'])); } } + /** + * Limit + */ if (isset($data['limit'])) { if (is_array($data['limit'])) { - $qb->limit = Limit::fromArray($data['limit']); + $this->limit = $this->createLimit($data['limit']['limit'] ?? null, $data['limit']['offset'] ?? null); } else { /** * Limit may also be provided directly in data. So does offset. */ - $limit = [ - 'limit' => $data['limit'], - 'offset' => $data['offset'] ?? null - ]; - $qb->limit = Limit::fromArray($limit); + $this->limit = $this->createLimit($data['limit'] ?? null, $data['offset'] ?? null); } } - $qb->parameters = $data['parameters'] ?? []; - return $qb; + + /** + * Parameters + */ + if (isset($data['parameters']) && is_array($data['parameters'])) { + $this->parameters = $data['parameters']; + } + + return $this; } - /** - * @return array{ - * class: string, - * type: string, - * select: array, - * from: array, - * columns: string[], - * where: null|array, - * orderBy: array, - * limit: array{limit?: int|null, offset?: int|null}|null, - * parameters: array - * } - */ + /** @return QueryBuilderToArray */ public function toArray(): array { return [ - 'class' => $this::class, - 'type' => $this->type->value, - 'select' => array_map(fn($expr) => $expr->toArray(), $this->select), - 'from' => array_map(fn($expr) => $expr->toArray(), $this->from), - 'join' => array_map(fn($expr) => $expr->toArray(), $this->joins), - 'columns' => $this->columns, - 'where' => $this->where === null ? null : $this->where->toArray(), - 'orderBy' => array_map(fn($expr) => $expr->toArray(), $this->orderBy), - 'limit' => ($this->limit === null ? null : $this->limit->toArray()), + 'type' => $this->type->value, + 'select' => array_map(fn($expr) => $expr->toArray(), $this->select), + 'from' => array_map(fn($expr) => $expr->toArray(), $this->from), + 'join' => array_map(fn($expr) => $expr->toArray(), $this->join), + 'columns' => $this->columns, + 'where' => $this->where === null ? null : $this->where->toArray(), + 'orderBy' => array_map(fn($expr) => $expr->toArray(), $this->orderBy), + 'limit' => ($this->limit === null ? null : $this->limit->toArray()), 'parameters' => $this->parameters ]; } @@ -268,18 +224,22 @@ public function toArray(): array /** * Start select query * - * ```php - * $qb->select('colA as A', 'colB as B'); - * ``` - * + * @param string|SelectFromArray|SelectExpression $exprs * @see self::createSelectExpression */ - public function select(string|SelectExpression ...$exprs): static + public function select(string|array|SelectExpression ...$exprs): static { $this->setType(Type::SELECT); foreach ($exprs as $expr) { if ($expr instanceof SelectExpression) { $this->addSelectExpression($expr); + } elseif (is_array($expr)) { + $this->addSelectExpression( + $this->createSelectExpression( + $expr['expression'] ?? '', + $expr['alias'] ?? null + ) + ); } else { $this->addSelectExpression($this->createSelectExpression($expr)); } @@ -294,7 +254,7 @@ public function select(string|SelectExpression ...$exprs): static * $qb->update('table')->values(['col' => 'val'])->where(...); * ``` */ - public function update(string|QueryBuilderInterface $from, string $as = null): static + public function update(string|FromExpression|QueryBuilderInterface $from, string $as = null): static { $this->setType(Type::UPDATE); return $this->from($from, $as); @@ -307,7 +267,7 @@ public function update(string|QueryBuilderInterface $from, string $as = null): s * $qb->insert('table')->values(['col' => 'val']); * ``` */ - public function insert(string $into, string $as = null): static + public function insert(string|FromExpression $into, string $as = null): static { $this->setType(Type::INSERT); return $this->into($into, $as); @@ -320,7 +280,7 @@ public function insert(string $into, string $as = null): static * $db->delete('table')->where('id = ?')->setParameters([1]); * ``` */ - public function delete(string|QueryBuilderInterface $from, string $as = null): static + public function delete(string|FromExpression|QueryBuilderInterface $from, string $as = null): static { $this->setType(Type::DELETE); return $this->from($from, $as); @@ -346,8 +306,11 @@ public function from(string|QueryBuilderInterface|FromExpression $from, string $ * * @see self::createFromExpression() */ - public function into(string $from, string $as = null): static + public function into(string|FromExpression $from, string $as = null): static { + if ($from instanceof FromExpression) { + return $this->addFromExpression($from); + } return $this->addFromExpression($this->createFromExpression($from, $as)); } @@ -386,22 +349,22 @@ public function set(array $values): static public function join(string $table, string $on, string $alias = null, JoinType $type = JoinType::INNER): static { - return $this->addJoinExpression($this->createJoinExpression($table, $on, $alias, $type)); + return $this->addJoinExpression($this->createJoinExpression($type, $table, $on, $alias)); } public function leftJoin(string $table, string $on, string $alias = null): static { - return $this->addJoinExpression($this->createJoinExpression($table, $on, $alias, JoinType::LEFT)); + return $this->addJoinExpression($this->createJoinExpression(JoinType::LEFT, $table, $on, $alias)); } public function rightJoin(string $table, string $on, string $alias = null): static { - return $this->addJoinExpression($this->createJoinExpression($table, $on, $alias, JoinType::RIGHT)); + return $this->addJoinExpression($this->createJoinExpression(JoinType::RIGHT, $table, $on, $alias)); } public function fullJoin(string $table, string $on, string $alias = null): static { - return $this->addJoinExpression($this->createJoinExpression($table, $on, $alias, JoinType::FULL)); + return $this->addJoinExpression($this->createJoinExpression(JoinType::FULL, $table, $on, $alias)); } /** @@ -558,6 +521,7 @@ public function getSql(): string case Type::UPDATE: return $this->type->value . $this->getSqlPartTable() . + $this->getSqlPartJoin() . $this->getSqlPartSet() . $this->getSqlPartWhere() . $this->getSqlPartOrderBy() . @@ -615,7 +579,7 @@ protected function getSqlPartFrom(): string protected function getSqlPartJoin(): string { - return empty($this->joins) ? '' : ' ' . implode(' ', array_map(fn($join) => (string)$join, $this->joins)); + return empty($this->join) ? '' : ' ' . implode(' ', array_map(fn($join) => (string)$join, $this->join)); } /** @@ -679,7 +643,7 @@ protected function setType(string|Type $type): static protected function reset(): static { - $this->select = $this->from = $this->columns = $this->parameters = $this->orderBy = []; + $this->select = $this->from = $this->columns = $this->join = $this->parameters = $this->orderBy = []; $this->limit = $this->where = null; return $this; } @@ -689,9 +653,9 @@ protected function reset(): static * * @see self::select() */ - protected function createSelectExpression(string $expr): SelectExpression + public function createSelectExpression(string $expr, string $alias = null): SelectExpression { - return SelectExpression::fromString($expr); + return new SelectExpression($expr, $alias); } /** @@ -708,7 +672,7 @@ public function addSelectExpression(SelectExpression $expr): static * * @see self::from() */ - protected function createFromExpression(string|QueryBuilderInterface $from, string $as = null): FromExpression + public function createFromExpression(string|QueryBuilderInterface $from, string $as = null): FromExpression { return new FromExpression($from, $as); } @@ -719,18 +683,18 @@ public function addFromExpression(FromExpression $expr): static return $this; } - protected function createJoinExpression( + public function createJoinExpression( + string|JoinType $type, string $table, string $on, string $alias = null, - JoinType $type = JoinType::INNER ): JoinExpression { return new JoinExpression($type, $table, $on, $alias); } public function addJoinExpression(JoinExpression $expr): static { - $this->joins[] = $expr; + $this->join[] = $expr; return $this; } @@ -752,8 +716,29 @@ public function addOrderByExpression(OrderByExpression $expr): static * * @see self::limit() */ - protected function createLimit(?int $limit, ?int $offset): Limit + public function createLimit(?int $limit, ?int $offset): Limit { return new Limit($limit, $offset); } + + /** + * Quote identifier + */ + public function quoteIdentifier(string $id): string + { + if (strpos($id, '.') !== false) { + return ( + implode( + '.', + array_map($this->quoteSingleIdentifier(...), explode('.', $id)) + ) + ); + } + return $this->quoteSingleIdentifier($id); + } + + public function quoteSingleIdentifier(string $id): string + { + return $this->quoteChar . $id . $this->quoteChar; + } } diff --git a/src/QueryBuilderInterface.php b/src/QueryBuilderInterface.php index 7d092fa..bf429c1 100644 --- a/src/QueryBuilderInterface.php +++ b/src/QueryBuilderInterface.php @@ -5,6 +5,7 @@ namespace Blrf\Dbal; use Blrf\Dbal\Query\Condition; +use Blrf\Dbal\Query\ConditionBuilder; use Blrf\Dbal\Query\ConditionGroup; use Blrf\Dbal\Query\FromExpression; use Blrf\Dbal\Query\JoinExpression; @@ -12,27 +13,87 @@ use Blrf\Dbal\Query\OrderByExpression; use Blrf\Dbal\Query\OrderByType; use Blrf\Dbal\Query\SelectExpression; - +use Blrf\Dbal\Query\Limit; + +/** + * Query builder interface + * + * @phpstan-import-type SelectFromArray from SelectExpression + * @phpstan-import-type SelectToArray from SelectExpression + * @phpstan-import-type FromFromArray from FromExpression + * @phpstan-import-type FromToArray from FromExpression + * @phpstan-import-type JoinFromArray from JoinExpression + * @phpstan-import-type JoinToArray from JoinExpression + * @phpstan-import-type OrderByFromArray from OrderByExpression + * @phpstan-import-type OrderByToArray from OrderByExpression + * @phpstan-import-type LimitFromArray from Limit + * @phpstan-import-type LimitToArray from Limit + * + * @phpstan-type QueryBuilderFromArray array{ + * type: string, + * select?: array|array, + * from?: array|array, + * join?: array|array, + * where?: array|null, + * orderBy?: array|array, + * limit?: int|null|LimitFromArray, + * offset?: int|null + * } + * + * @phpstan-type QueryBuilderToArray array{ + * type: string, + * select: array, + * from: array, + * join: array, + * where: array|null, + * orderBy: array, + * limit: null|LimitToArray + * } + */ interface QueryBuilderInterface { /** - * Create query builder from array + * Build query from array * - * @param array $data + * @param QueryBuilderFromArray $data + */ + public function fromArray(array $data): static; + /** + * Return query builder as array + * @return QueryBuilderToArray */ - public static function fromArray(array $data, mixed ...$arguments): self; + public function toArray(): array; - public function select(string|SelectExpression ...$exprs): static; + /** + * @param string|SelectFromArray|SelectExpression $exprs + */ + public function select(string|array|SelectExpression ...$exprs): static; - public function update(string|self $from): static; + public function update(string|FromExpression|self $from): static; - public function insert(string $into): static; + public function insert(string|FromExpression $into): static; - public function delete(string|self $from): static; + public function delete(string|FromExpression|self $from): static; public function from(string|FromExpression|self $from, string $as = null): static; - public function addFromExpression(FromExpression $expr): static; + public function into(string|FromExpression $from, string $as = null): static; + + public function value(string $column, mixed $value): static; + + /** + * Add values for insert or update + * + * @param array $values [ column => value, ...] + */ + public function values(array $values): static; + + /** + * Set (self::values() alias) + * + * @param array $values + */ + public function set(array $values): static; public function join(string $table, string $on, string $alias = null, JoinType $type = JoinType::INNER): static; @@ -40,16 +101,16 @@ public function leftJoin(string $table, string $on, string $alias = null): stati public function rightJoin(string $table, string $on, string $alias = null): static; public function fullJoin(string $table, string $on, string $alias = null): static; - public function addJoinExpression(JoinExpression $expr): static; - - public function value(string $column, mixed $value): static; - /** - * Add values for insert or update + * Start condition builder or create simple condition * - * @param array $values [ column => value, ...] + * @return ($expression is null ? ConditionBuilder : Condition) */ - public function values(array $values): static; + public function condition( + string $expression = null, + string $operator = '=', + string $value = '?' + ): Condition|ConditionBuilder; public function where(Condition|ConditionGroup|callable $condition): static; @@ -59,8 +120,6 @@ public function orWhere(Condition|ConditionGroup|callable $condition): static; public function orderBy(string $orderBy, OrderByType|string $type = 'ASC'): static; - public function addOrderByExpression(OrderByExpression $expr): static; - public function limit(?int $offset = null, ?int $limit = null): static; /** @@ -77,12 +136,35 @@ public function addParameter(mixed ...$param): static; */ public function getParameters(): array; + public function getSql(): string; + /** - * To array - * - * @return array + * Quote identifier */ - public function toArray(): array; + public function quoteIdentifier(string $id): string; - public function getSql(): string; + /** + * Creators + */ + public function createSelectExpression(string $expr, string $alias = null): SelectExpression; + public function createFromExpression(string|QueryBuilderInterface $from, string $as = null): FromExpression; + public function createJoinExpression( + string|JoinType $type, + string $table, + string $on, + string $alias = null, + ): JoinExpression; + public function createOrderByExpression( + string $expr, + OrderByType|string $type = 'ASC' + ): OrderByExpression; + public function createLimit(?int $limit, ?int $offset): Limit; + + /** + * Adders + */ + public function addSelectExpression(SelectExpression $expr): static; + public function addFromExpression(FromExpression $expr): static; + public function addJoinExpression(JoinExpression $expr): static; + public function addOrderByExpression(OrderByExpression $expr): static; } diff --git a/src/Result.php b/src/Result.php index f3d5c08..551392a 100644 --- a/src/Result.php +++ b/src/Result.php @@ -5,12 +5,18 @@ namespace Blrf\Dbal; use Countable; +use Iterator; +use count; /** * Result class + * + * @implements Iterator> */ -class Result implements Countable +class Result implements Countable, Iterator { + protected int $position = 0; + /** * Constructor * @@ -28,4 +34,32 @@ public function count(): int { return count($this->rows); } + + /** + * ITERATOR METHODS + */ + public function rewind(): void + { + $this->position = 0; + } + + public function current(): array + { + return $this->rows[$this->position]; + } + + public function key(): mixed + { + return $this->position; + } + + public function next(): void + { + ++$this->position; + } + + public function valid(): bool + { + return isset($this->rows[$this->position]); + } } diff --git a/tests/Query/FromExpressionTest.php b/tests/Query/FromExpressionTest.php index 7ca6adb..6070c4d 100644 --- a/tests/Query/FromExpressionTest.php +++ b/tests/Query/FromExpressionTest.php @@ -16,35 +16,22 @@ public function testConstructWithEmptyExpressionThrowsValueError(): void new FromExpression(''); } - public function testFromArrayAndToArraySimple(): void + public function testConstructWithQueryBuilderAndEmptyAlias(): void { - $a = ['expression' => 'MyExpression', 'alias' => 'MyAlias']; - $e = FromExpression::fromArray($a); - $this->assertSame($a, $e->toArray()); - } - - public function testFromArrayAndToArraySubquery(): void - { - $a = [ - 'expression' => (new QueryBuilder())->toArray(), - 'alias' => 'myAlias' - ]; - $e = FromExpression::fromArray($a); - $this->assertSame($a, $e->toArray()); + $this->expectException(\ValueError::class); + new FromExpression(new QueryBuilder()); } - public function testFromStringAndToStringWithAlias(): void + public function testToStringWithoutAlias(): void { - $s = 'from AS alias'; - $e = FromExpression::fromString($s); - $this->assertSame($s, (string)$e); + $e = new FromExpression('from'); + $this->assertSame('from', (string)$e); } - public function testFromStringAndToStringWithoutAlias(): void + public function testToStringWithAlias(): void { - $s = 'from'; - $e = FromExpression::fromString($s); - $this->assertSame($s, (string)$e); + $e = new FromExpression('from', 'alias'); + $this->assertSame('from AS alias', (string)$e); } public function testToStringWithSubquery(): void @@ -52,4 +39,34 @@ public function testToStringWithSubquery(): void $e = new FromExpression(new QueryBuilder(), 't1'); $this->assertSame('(SELECT ) AS t1', (string)$e); } + + public function testToArray(): void + { + $e = new FromExpression('from', 'alias'); + $exp = [ + 'expression' => 'from', + 'alias' => 'alias' + ]; + $this->assertSame($exp, $e->toArray()); + } + + public function testToArrayWithQueryBuilder(): void + { + $e = new FromExpression(new QueryBuilder(), 'alias'); + $exp = [ + 'expression' => [ + 'type' => 'SELECT', + 'select' => [], + 'from' => [], + 'join' => [], + 'columns' => [], + 'where' => null, + 'orderBy' => [], + 'limit' => null, + 'parameters' => [], + ], + 'alias' => 'alias' + ]; + $this->assertSame($exp, $e->toArray()); + } } diff --git a/tests/Query/JoinExpressionTest.php b/tests/Query/JoinExpressionTest.php index adac03b..8544ce3 100644 --- a/tests/Query/JoinExpressionTest.php +++ b/tests/Query/JoinExpressionTest.php @@ -28,39 +28,27 @@ public function testConstructDefaultTypeIsInner(): void $this->assertSame($expr->type, JoinType::INNER); } - public function testFromStringNotImplemented(): void + public function testToStringWithoutAlias(): void { - $this->expectException(\Exception::class); - $exp = JoinExpression::fromString(''); + $expr = new JoinExpression(JoinType::LEFT, 'table', 'on'); + $this->assertSame('LEFT JOIN table ON on', $expr->__toString()); } - public function testFromArrayAndToArrayAndToString(): void + public function testToStringWithAlias(): void { - $expr = JoinExpression::fromArray([ - 'type' => JoinType::FULL, - 'table' => 'table', - 'on' => 'on', - 'alias' => 'alias' - ]); - $this->assertSame(JoinType::FULL, $expr->type); - $this->assertSame('table', $expr->table); - $this->assertSame('on', $expr->on); - $this->assertSame('alias', $expr->alias); + $expr = new JoinExpression(JoinType::LEFT, 'table', 'on', 'alias'); + $this->assertSame('LEFT JOIN table AS alias ON on', $expr->__toString()); + } + public function testToArray(): void + { + $expr = new JoinExpression(JoinType::FULL, 'table', 'on', 'alias'); $exp = [ 'type' => JoinType::FULL->value, 'table' => 'table', 'on' => 'on', 'alias' => 'alias' ]; - $this->assertSame($exp, $expr->toArray()); - $this->assertSame('FULL JOIN table AS alias ON on', $expr->__toString()); - } - - public function testToStringWithoutAlias():void - { - $expr = new JoinExpression(JoinType::LEFT, 'table', 'on'); - $this->assertSame('LEFT JOIN table ON on', $expr->__toString()); } } diff --git a/tests/Query/LimitTest.php b/tests/Query/LimitTest.php index 6ee2c9d..e2f84dd 100644 --- a/tests/Query/LimitTest.php +++ b/tests/Query/LimitTest.php @@ -15,24 +15,14 @@ public function testConstructWithNoArguments(): void new Limit(null); } - public function testFromArrayAndToArray(): void + public function testWithLimitAndOffset(): void { - $a = ['limit' => 10, 'offset' => 11]; - $l = Limit::fromArray($a); - $this->assertSame($a, $l->toArray()); - } - - public function testFromStringAndToStringWithOffset(): void - { - $s = 'LIMIT 10 OFFSET 20'; - $l = Limit::fromString($s); - $this->assertSame($s, (string)$l); - } - - public function testFromStringAndToStringWithoutOffset(): void - { - $s = 'LIMIT 10'; - $l = Limit::fromString($s); - $this->assertSame($s, (string)$l); + $l = new Limit(1, 2); + $this->assertSame('LIMIT 1 OFFSET 2', (string)$l); + $exp = [ + 'limit' => 1, + 'offset' => 2 + ]; + $this->assertSame($exp, $l->toArray()); } } diff --git a/tests/Query/OrderByExpressionTest.php b/tests/Query/OrderByExpressionTest.php index 30204b5..1722879 100644 --- a/tests/Query/OrderByExpressionTest.php +++ b/tests/Query/OrderByExpressionTest.php @@ -15,27 +15,14 @@ public function testConstructWithEmptyExpressionThrowsValueError(): void new OrderByExpression(''); } - public function testFromArrayAndToArray(): void + public function testWithStringType(): void { - $a = [ - 'expression' => 'MyExpression', - 'type' => 'DESC' + $o = new OrderByExpression('expr', 'asc'); + $this->assertSame('expr ASC', (string)$o); + $exp = [ + 'expression' => 'expr', + 'type' => 'ASC' ]; - $e = OrderByExpression::fromArray($a); - $this->assertSame($a, $e->toArray()); - } - - public function testFromStringAndToStringWithType(): void - { - $s = 'MyExpression DESC'; - $e = OrderByExpression::fromString($s); - $this->assertSame($s, (string)$e); - } - - public function testFromStringAndToStringWithoutType(): void - { - $s = 'MyExpression'; - $e = OrderByExpression::fromString($s); - $this->assertSame($s . ' ASC', (string)$e); + $this->assertSame($exp, $o->toArray()); } } diff --git a/tests/Query/SelectExpressionTest.php b/tests/Query/SelectExpressionTest.php index 83c872b..538f4c3 100644 --- a/tests/Query/SelectExpressionTest.php +++ b/tests/Query/SelectExpressionTest.php @@ -15,27 +15,25 @@ public function testConstructWithEmptyExpressionThrowsValueError(): void new SelectExpression(''); } - public function testFromArrayAndToArray(): void + public function testSelectWithoutAlias(): void { - $a = [ - 'expression' => 'MyExpression', - 'alias' => 'MyAlias' + $s = new SelectExpression('expr'); + $this->assertSame('expr', (string)$s); + $exp = [ + 'expression' => 'expr', + 'alias' => null ]; - $e = SelectExpression::fromArray($a); - $this->assertSame($a, $e->toArray()); + $this->assertSame($exp, $s->toArray()); } - public function testFromStringAndToStringWithAlias(): void + public function testSelectWithAlias(): void { - $s = 'MyExpression AS MyAlias'; - $e = SelectExpression::fromString($s); - $this->assertSame($s, (string)$e); - } - - public function testFromStringAndToStringWithoutAlias(): void - { - $s = '1+1'; - $e = SelectExpression::fromString($s); - $this->assertSame($s, (string)$e); + $s = new SelectExpression('expr', 'alias'); + $this->assertSame('expr AS alias', (string)$s); + $exp = [ + 'expression' => 'expr', + 'alias' => 'alias' + ]; + $this->assertSame($exp, $s->toArray()); } } diff --git a/tests/QueryBuilderTest.php b/tests/QueryBuilderTest.php index 0027eb2..359e13f 100644 --- a/tests/QueryBuilderTest.php +++ b/tests/QueryBuilderTest.php @@ -5,6 +5,7 @@ use Blrf\Dbal\QueryBuilder; use Blrf\Dbal\Query\Condition; use Blrf\Dbal\Query\FromExpression; +use Blrf\Dbal\Query\JoinExpression; use Blrf\Dbal\Query\OrderByExpression; use Blrf\Dbal\Query\SelectExpression; use PHPUnit\Framework\Attributes\CoversClass; @@ -18,7 +19,6 @@ public function testEmptyQueryBuilder(): void $this->assertSame('SELECT ', $qb->getSql()); $a = $qb->toArray(); $exp = [ - 'class' => QueryBuilder::class, 'type' => 'SELECT', 'select' => [], 'from' => [], @@ -31,13 +31,62 @@ public function testEmptyQueryBuilder(): void ]; $this->assertSame($exp, $a); - $new = QueryBuilder::fromArray($a); - $this->assertEquals($qb, $new); + $qb->fromArray($a); + $this->assertEquals($exp, $qb->toArray()); } public function testSelect(): void { - $exp = 'SELECT a,b FROM c INNER JOIN d AS e ON c.id = e.id WHERE f = ? ORDER BY f ASC LIMIT 1 OFFSET 2'; + $exp = 'SELECT a,b FROM c INNER JOIN d AS e ON c.id = e.id WHERE f = ? ORDER BY f ASC, x DESC LIMIT 1 OFFSET 2'; + + $expArray = [ + 'type' => 'SELECT', + 'select' => [ + [ + 'expression' => 'a', + 'alias' => null + ], + [ + 'expression' => 'b', + 'alias' => null + ] + ], + 'from' => [ + [ + 'expression' => 'c', + 'alias' => null + ] + ], + 'join' => [ + [ + 'type' => 'INNER', + 'table' => 'd', + 'on' => 'c.id = e.id', + 'alias' => 'e' + ] + ], + 'columns' => [], + 'where' => [ + 'expression' => 'f', + 'operator' => '=', + 'value' => '?' + ], + 'orderBy' => [ + [ + 'expression' => 'f', + 'type' => 'ASC' + ], + [ + 'expression' => 'x', + 'type' => 'DESC' + ] + ], + 'limit' => [ + 'limit' => 1, + 'offset' => 2 + ], + 'parameters' => ['h'] + ]; $qb = new QueryBuilder(); $qb ->select('a', 'b') @@ -47,16 +96,20 @@ public function testSelect(): void fn($b) => $b->eq('f') ) ->orderBy('f') + ->orderBy(new OrderByExpression('x', 'DESC')) ->limit(1, 2) ->setParameters(['h']); $this->assertSame( $exp, $qb->getSql() ); + $a = $qb->toArray(); + $this->assertSame($expArray, $a); + $this->assertSame(['h'], $qb->getParameters()); + $qb = $qb->fromArray($qb->toArray()); + $this->assertSame($exp, $qb->getSql()); $this->assertSame(['h'], $qb->getParameters()); - $nqb = QueryBuilder::fromArray($qb->toArray()); - $this->assertSame($exp, $nqb->getSql()); - $this->assertSame(['h'], $nqb->getParameters()); + $this->assertSame($expArray, $qb->toArray()); } public function testSelectLeftJoin(): void @@ -78,9 +131,9 @@ public function testSelectLeftJoin(): void $qb->getSql() ); $this->assertSame(['h'], $qb->getParameters()); - $nqb = QueryBuilder::fromArray($qb->toArray()); - $this->assertSame($exp, $nqb->getSql()); - $this->assertSame(['h'], $nqb->getParameters()); + $qb = $qb->fromArray($qb->toArray()); + $this->assertSame($exp, $qb->getSql()); + $this->assertSame(['h'], $qb->getParameters()); } public function testSelectRightJoinWithoutAlias(): void @@ -102,9 +155,9 @@ public function testSelectRightJoinWithoutAlias(): void $qb->getSql() ); $this->assertSame(['h'], $qb->getParameters()); - $nqb = QueryBuilder::fromArray($qb->toArray()); - $this->assertSame($exp, $nqb->getSql()); - $this->assertSame(['h'], $nqb->getParameters()); + $qb = $qb->fromArray($qb->toArray()); + $this->assertSame($exp, $qb->getSql()); + $this->assertSame(['h'], $qb->getParameters()); } public function testSelectFullJoin(): void @@ -126,9 +179,9 @@ public function testSelectFullJoin(): void $qb->getSql() ); $this->assertSame(['h'], $qb->getParameters()); - $nqb = QueryBuilder::fromArray($qb->toArray()); - $this->assertSame($exp, $nqb->getSql()); - $this->assertSame(['h'], $nqb->getParameters()); + $qb = $qb->fromArray($qb->toArray()); + $this->assertSame($exp, $qb->getSql()); + $this->assertSame(['h'], $qb->getParameters()); } public function testSelectWithExpression(): void @@ -158,9 +211,9 @@ public function testSelectWithAddWhereWithoutPreviousWhere(): void $qb->getSql() ); $this->assertSame(['f'], $qb->getParameters()); - $nqb = QueryBuilder::fromArray($qb->toArray()); - $this->assertSame($exp, $nqb->getSql()); - $this->assertSame(['f'], $nqb->getParameters()); + $qb = $qb->fromArray($qb->toArray()); + $this->assertSame($exp, $qb->getSql()); + $this->assertSame(['f'], $qb->getParameters()); } public function testSelectWithAddWhereWithPreviousWhere(): void @@ -183,9 +236,9 @@ public function testSelectWithAddWhereWithPreviousWhere(): void $qb->getSql() ); $this->assertSame(['f', 'h'], $qb->getParameters()); - $nqb = QueryBuilder::fromArray($qb->toArray()); - $this->assertSame($exp, $nqb->getSql()); - $this->assertSame(['f', 'h'], $nqb->getParameters()); + $qb = $qb->fromArray($qb->toArray()); + $this->assertSame($exp, $qb->getSql()); + $this->assertSame(['f', 'h'], $qb->getParameters()); } public function testSelectWithOrWhereWithoutPreviousWhere(): void @@ -206,9 +259,9 @@ public function testSelectWithOrWhereWithoutPreviousWhere(): void $qb->getSql() ); $this->assertSame(['f'], $qb->getParameters()); - $nqb = QueryBuilder::fromArray($qb->toArray()); - $this->assertSame($exp, $nqb->getSql()); - $this->assertSame(['f'], $nqb->getParameters()); + $qb = $qb->fromArray($qb->toArray()); + $this->assertSame($exp, $qb->getSql()); + $this->assertSame(['f'], $qb->getParameters()); } public function testSelectWithOrWhereWithPreviousWhere(): void @@ -231,9 +284,9 @@ public function testSelectWithOrWhereWithPreviousWhere(): void $qb->getSql() ); $this->assertSame(['f', 'h'], $qb->getParameters()); - $nqb = QueryBuilder::fromArray($qb->toArray()); - $this->assertSame($exp, $nqb->getSql()); - $this->assertSame(['f', 'h'], $nqb->getParameters()); + $qb = $qb->fromArray($qb->toArray()); + $this->assertSame($exp, $qb->getSql()); + $this->assertSame(['f', 'h'], $qb->getParameters()); } public function testUpdate(): void @@ -252,9 +305,9 @@ public function testUpdate(): void ->limit(1); $this->assertSame($exp, $qb->getSql()); $this->assertSame(['c', 'e', 'g'], $qb->getParameters()); - $nqb = QueryBuilder::fromArray($qb->toArray()); - $this->assertSame($exp, $nqb->getSql()); - $this->assertSame(['c', 'e', 'g'], $nqb->getParameters()); + $qb = $qb->fromArray($qb->toArray()); + $this->assertSame($exp, $qb->getSql()); + $this->assertSame(['c', 'e', 'g'], $qb->getParameters()); } public function testInsert(): void @@ -269,9 +322,26 @@ public function testInsert(): void ]); $this->assertSame($exp, $qb->getSql()); $this->assertSame(['c', 'f'], $qb->getParameters()); - $nqb = QueryBuilder::fromArray($qb->toArray()); - $this->assertSame($exp, $nqb->getSql()); - $this->assertSame(['c', 'f'], $nqb->getParameters()); + $qb = $qb->fromArray($qb->toArray()); + $this->assertSame($exp, $qb->getSql()); + $this->assertSame(['c', 'f'], $qb->getParameters()); + } + + public function testInsertWithFromExpression(): void + { + $exp = 'INSERT INTO a (b, d) VALUES(?, ?)'; + $qb = new QueryBuilder(); + $qb + ->insert(new FromExpression('a')) + ->values([ + 'b' => 'c', + 'd' => 'f' + ]); + $this->assertSame($exp, $qb->getSql()); + $this->assertSame(['c', 'f'], $qb->getParameters()); + $qb = $qb->fromArray($qb->toArray()); + $this->assertSame($exp, $qb->getSql()); + $this->assertSame(['c', 'f'], $qb->getParameters()); } public function testDelete(): void @@ -290,101 +360,68 @@ public function testDelete(): void ->orderBy('f', 'DESC') ->limit(5); $this->assertSame($exp, $qb->getSql()); - $nqb = QueryBuilder::fromArray($qb->toArray()); - $this->assertSame($exp, $nqb->getSql()); - } - - public function testFromArrayClassNotQueryBuilder(): void - { - $this->expectException(\TypeError::class); - $data = [ - 'class' => \StdClass::class - ]; - QueryBuilder::fromArray($data); - } - - public function testFromArraySelectIsString(): void - { - $data = [ - 'select' => '1' - ]; - $qb = QueryBuilder::fromArray($data); - $exp = 'SELECT 1'; - $this->assertSame($exp, $qb->getSql()); - } - - public function testFromArraySelectIsArrayWithString(): void - { - $data = [ - 'select' => ['1'] - ]; - $qb = QueryBuilder::fromArray($data); - $exp = 'SELECT 1'; + $qb = $qb->fromArray($qb->toArray()); $this->assertSame($exp, $qb->getSql()); } - public function testFromArrayFromIsString(): void + public function testFromArrayWithoutType(): void { + $this->expectException(\ValueError::class); $data = [ - 'select' => '1', - 'from' => 'table' - ]; - $qb = QueryBuilder::fromArray($data); - $exp = 'SELECT 1 FROM table'; - $this->assertSame($exp, $qb->getSql()); - } - - public function testFromArrayFromIsArrayWithString(): void - { - $data = [ - 'select' => '1', - 'from' => ['table'] + 'select' => '1' ]; - $qb = QueryBuilder::fromArray($data); - $exp = 'SELECT 1 FROM table'; - $this->assertSame($exp, $qb->getSql()); + // @phpstan-ignore-next-line + $qb = (new QueryBuilder())->fromArray($data); } - public function testFromArrayOrderByIsString(): void + public function testFromArrayFromIsQueryBuilderArray(): void { $data = [ - 'select' => '1', - 'from' => 'table', - 'orderBy' => 'column ASC' + 'type' => 'SELECT', + 'from' => [ + [ + 'expression' => [ + 'type' => 'SELECT', + 'select' => [ + ['expression' => '2'] + ] + ], + 'alias' => 'alias' + ] + ] ]; - $qb = QueryBuilder::fromArray($data); - $exp = 'SELECT 1 FROM table ORDER BY column ASC'; - $this->assertSame($exp, $qb->getSql()); + $qb = (new QueryBuilder())->fromArray($data); + $this->assertSame('SELECT FROM (SELECT 2) AS alias', $qb->getSql()); } - public function testFromArrayOrderByIsArrayWithString(): void + public function testFromArrayWithExpressionObjects(): void { + $exp = 'SELECT 1 AS number FROM table AS alias INNER JOIN table AS jalias ON on ' . + 'ORDER BY number DESC LIMIT 1 OFFSET 2'; $data = [ - 'select' => '1', - 'from' => 'table', - 'orderBy' => ['column ASC'] + 'type' => 'SELECT', + 'select' => [new SelectExpression('1', 'number')], + 'from' => [new FromExpression('table', 'alias')], + 'join' => [new JoinExpression('INNER', 'table', 'on', 'jalias')], + 'orderBy' => [new OrderByExpression('number', 'DESC')], + 'limit' => 1, + 'offset' => 2 ]; - $qb = QueryBuilder::fromArray($data); - $exp = 'SELECT 1 FROM table ORDER BY column ASC'; + $qb = (new QueryBuilder())->fromArray($data); $this->assertSame($exp, $qb->getSql()); } - public function testFromArrayLimitDirectlyInData(): void + public function testQuoteIdentifierSingle(): void { - $data = [ - 'select' => '1', - 'limit' => 1, - 'offset' => 2 - ]; - $qb = QueryBuilder::fromArray($data); - $exp = 'SELECT 1 LIMIT 1 OFFSET 2'; - $this->assertSame($exp, $qb->getSql()); + $id = 'foobar'; + $qb = new QueryBuilder(); + $this->assertSame('`foobar`', $qb->quoteIdentifier($id)); } - public function testOrderByWithOrderByExpression(): void + public function testQuoteIdentifiers(): void { + $id = 'foo.bar.id'; $qb = new QueryBuilder(); - $qb->orderBy(new OrderByExpression('expr', 'ASC')); - $this->assertSame('SELECT ORDER BY expr ASC', $qb->getSql()); + $this->assertSame('`foo`.`bar`.`id`', $qb->quoteIdentifier($id)); } } diff --git a/tests/ResultTest.php b/tests/ResultTest.php index 2e984e9..a6a79a4 100644 --- a/tests/ResultTest.php +++ b/tests/ResultTest.php @@ -13,4 +13,21 @@ public function testCountable(): void $result = new Result(); $this->assertSame(0, count($result)); } + + public function testIterator() + { + $result = new Result([['column' => 'value']]); + $this->assertSame(1, count($result)); + foreach ($result as $row) { + $this->assertSame( + [ + 'column' => 'value' + ], + $row + ); + } + $this->assertSame(1, $result->key()); + $result->rewind(); + $this->assertSame(0, $result->key()); + } }