diff --git a/src/Builder/Builder.php b/src/Builder/Builder.php index 1818bcf..005d65d 100644 --- a/src/Builder/Builder.php +++ b/src/Builder/Builder.php @@ -5,6 +5,7 @@ use Closure; use Illuminate\Support\Arr; use Illuminate\Support\Str; +use Illuminate\Support\Stringable; use NorbyBaru\AwsTimestream\Concerns\BuildersConcern; use NorbyBaru\AwsTimestream\Contract\QueryBuilderContract; @@ -34,6 +35,7 @@ abstract class Builder implements QueryBuilderContract protected string $table = ''; protected string $fromQuery = ''; protected string $whereQuery = ''; + protected string $havingQuery = ''; protected string $selectStatement = ''; protected string $orderByQuery = ''; protected string $groupByQuery = ''; @@ -124,41 +126,53 @@ public function whereRaw(string $statement): self public function where(string $column, $value, string $operator = '=', string $boolean = 'and', bool $ago = false): self { $query = Str::of($this->whereQuery); + $this->whereQuery = $this->modifyQueryPart('WHERE', $query, $column, $value, $operator, $boolean, $ago); + return $this; + } + + protected function modifyQueryPart( + string $sqlPart, + Stringable $query, + string $column, + $value, + string $operator = '=', + string $boolean = 'and', + bool $ago = false + ): Stringable { + if (!in_array($sqlPart, ['WHERE', 'HAVING'])) { + throw new \InvalidArgumentException(sprintf('Invalid sql part %s', $sqlPart)); + } $value = $value instanceof Closure // If the value is a Closure, it means the developer is performing an entire ? '(' . call_user_func($value) . ')' : $value; if ($query->length() == 0) { - $whereQuery = $query->append( - sprintf('WHERE %s %s %s', $column, $operator, $value) + $queryPart = $query->append( + sprintf($sqlPart . ' %s %s %s', $column, $operator, $value) ); if ($ago) { - $whereQuery = $query->append( - sprintf('WHERE %s %s ago(%s)', $column, $operator, $value) + $queryPart = $query->append( + sprintf($sqlPart . ' %s %s ago(%s)', $column, $operator, $value) ); } - $this->whereQuery = $whereQuery; - - return $this; + return $queryPart; } - $whereQuery = $query->append( + $queryPart = $query->append( sprintf(' %s %s %s %s', mb_strtoupper($boolean), $column, $operator, $value) ); if ($ago) { - $whereQuery = $query->append( + $queryPart = $query->append( sprintf(' %s %s %s ago(%s)', mb_strtoupper($boolean), $column, $operator, $value) ); } - $this->whereQuery = $whereQuery; - - return $this; + return $queryPart; } public function whereAgo(string $column, $value, string $operator = '=', string $boolean = 'and'): self @@ -300,6 +314,21 @@ public function whereNotNull(string|array $columns, $boolean = 'and'): self return $this->whereNull($columns, $boolean, true); } + public function havingRaw(string $statement): self + { + $this->havingQuery = $statement; + + return $this; + } + + public function having(string $column, $value, string $operator = '=', string $boolean = 'and', bool $ago = false): self + { + $query = Str::of($this->havingQuery); + $this->havingQuery = $this->modifyQueryPart('HAVING', $query, $column, $value, $operator, $boolean, $ago); + + return $this; + } + public function limitBy(int $limit): self { $this->limitByQuery = sprintf('LIMIT %s', $limit); diff --git a/src/Concerns/BuildersConcern.php b/src/Concerns/BuildersConcern.php index f346ed4..f3b6408 100644 --- a/src/Concerns/BuildersConcern.php +++ b/src/Concerns/BuildersConcern.php @@ -52,6 +52,11 @@ public function getOrderByQuery(): string return $this->orderByQuery; } + public function getHavingQuery(): string + { + return $this->havingQuery; + } + public function getGroupByQuery(): string { return $this->groupByQuery; @@ -108,6 +113,12 @@ public function getQueryString(): string ->append($this->getGroupByQuery()); } + if ($this->getHavingQuery()) { + $queryString = $queryString + ->append(' ') + ->append($this->getHavingQuery()); + } + if ($this->getOrderByQuery()) { $queryString = $queryString ->append(' ') diff --git a/tests/Unit/ReaderUnitTest.php b/tests/Unit/ReaderUnitTest.php index 8db47b0..4e19aee 100644 --- a/tests/Unit/ReaderUnitTest.php +++ b/tests/Unit/ReaderUnitTest.php @@ -82,7 +82,7 @@ public function test_query_builder_can_use_aliases_in_selects() public function test_query_builder_can_add_rawWhere() { - $sql = "SELECT p.name, avg(r.rating) AS avg_rating FROM \"shop\".\"products\" AS p LEFT JOIN \"shop\".\"reviews\" AS r ON p.id = r.product_id WHERE p.name = \"test\" AND avg_rating > 4 ORDER BY time desc LIMIT 10"; + $sql = "SELECT p.name, avg(r.rating) AS avg_rating FROM \"shop\".\"products\" AS p LEFT JOIN \"shop\".\"reviews\" AS r ON p.id = r.product_id WHERE p.name = \"test\" AND avg_rating > 4 GROUP BY p.name ORDER BY time desc LIMIT 10"; $queryBuilder = TimestreamBuilder::query() ->select('p.name, avg(r.rating) AS avg_rating') ->from("shop", 'products', 'p') @@ -90,6 +90,43 @@ public function test_query_builder_can_add_rawWhere() ->whereRaw("WHERE p.name = \"test\"") ->andWhere('avg_rating', 4, '>') ->orderBy('time', 'desc') + ->groupBy('p.name') + ->limitBy(10); + + $this->assertInstanceOf(Builder::class, $queryBuilder); + $this->assertIsString($queryBuilder->getSql()); + $this->assertEquals($queryBuilder->getSql(), $sql); + } + + public function test_query_build_can_add_having() + { + $sql = "SELECT p.name, avg(r.rating) AS avg_rating FROM \"shop\".\"products\" AS p LEFT JOIN \"shop\".\"reviews\" AS r ON p.id = r.product_id WHERE p.name = \"test\" GROUP BY p.name HAVING avg(r.rating) > 4 ORDER BY time desc LIMIT 10"; + $queryBuilder = TimestreamBuilder::query() + ->select('p.name, avg(r.rating) AS avg_rating') + ->from("shop", 'products', 'p') + ->leftJoin("shop", 'reviews', 'r', 'p.id = r.product_id') + ->whereRaw("WHERE p.name = \"test\"") + ->orderBy('time', 'desc') + ->groupBy('p.name') + ->having('avg(r.rating)', 4, '>') + ->limitBy(10); + + $this->assertInstanceOf(Builder::class, $queryBuilder); + $this->assertIsString($queryBuilder->getSql()); + $this->assertEquals($queryBuilder->getSql(), $sql); + } + + public function test_query_build_can_add_rawhaving() + { + $sql = "SELECT p.name, avg(r.rating) AS avg_rating FROM \"shop\".\"products\" AS p LEFT JOIN \"shop\".\"reviews\" AS r ON p.id = r.product_id WHERE p.name = \"test\" GROUP BY p.name HAVING avg(r.rating) > 4 ORDER BY time desc LIMIT 10"; + $queryBuilder = TimestreamBuilder::query() + ->select('p.name, avg(r.rating) AS avg_rating') + ->from("shop", 'products', 'p') + ->leftJoin("shop", 'reviews', 'r', 'p.id = r.product_id') + ->whereRaw("WHERE p.name = \"test\"") + ->orderBy('time', 'desc') + ->groupBy('p.name') + ->havingRaw('HAVING avg(r.rating) > 4') ->limitBy(10); $this->assertInstanceOf(Builder::class, $queryBuilder);