Skip to content

Commit 3f8c142

Browse files
committed
Add return aggregation support
1 parent c5b87ac commit 3f8c142

File tree

7 files changed

+319
-1
lines changed

7 files changed

+319
-1
lines changed
+6-1
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
<?php
22

3-
declare(strict_types=1);
3+
declare (strict_types = 1);
44

55
namespace Asseco\JsonQueryBuilder\RequestParameters;
66

7+
use Asseco\JsonQueryBuilder\Traits\DatabaseFunctions;
8+
79
class ReturnsParameter extends AbstractParameter
810
{
11+
use DatabaseFunctions;
12+
913
public static function getParameterName(): string
1014
{
1115
return 'returns';
1216
}
1317

1418
protected function appendQuery(): void
1519
{
20+
$this->prepareArguments();
1621
$this->builder->addSelect($this->arguments);
1722
}
1823
}

src/SQLProviders/PgSQLFunctions.php

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace Asseco\JsonQueryBuilder\SQLProviders;
4+
5+
class PgSQLFunctions extends SQLFunctions
6+
{
7+
public static function year($raw)
8+
{
9+
return "EXTRACT(YEAR FROM $raw)";
10+
}
11+
12+
public static function month($raw)
13+
{
14+
return "EXTRACT(MONTH FROM $raw)";
15+
}
16+
17+
public static function day($raw)
18+
{
19+
return "EXTRACT(DAY FROM $raw)";
20+
}
21+
}

src/SQLProviders/SQLFunctions.php

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
namespace Asseco\JsonQueryBuilder\SQLProviders;
4+
5+
use Asseco\JsonQueryBuilder\Exceptions\JsonQueryBuilderException;
6+
7+
class SQLFunctions
8+
{
9+
public const DB_FUNCTIONS = [
10+
"avg",
11+
"count",
12+
"max",
13+
"min",
14+
"sum",
15+
"distinct",
16+
17+
"year",
18+
"month",
19+
"day",
20+
];
21+
22+
final public static function validateArgument(string $argument): void
23+
{
24+
$split = explode(":", $argument);
25+
$column = array_pop($split);
26+
27+
if (!preg_match("/^[a-zA-Z_][a-zA-Z0-9_]*|\*$/", $column) || in_array($column, self::DB_FUNCTIONS)) {
28+
throw new JsonQueryBuilderException(
29+
"Invalid column name: {$column}."
30+
);
31+
}
32+
33+
if ($invalidFns = array_diff($split, self::DB_FUNCTIONS)) {
34+
throw new JsonQueryBuilderException(
35+
"Invalid function: " . join(",", $invalidFns) . "."
36+
);
37+
}
38+
}
39+
40+
final public static function __callStatic($fn, $args)
41+
{
42+
if (!in_array($fn, self::DB_FUNCTIONS)) {
43+
throw new JsonQueryBuilderException(
44+
"Invalid function: $fn."
45+
);
46+
}
47+
48+
if (method_exists(self::class, $fn)) {
49+
return self::$fn($args);
50+
}
51+
52+
return $fn . "($args[0])";
53+
}
54+
}

src/Traits/DatabaseFunctions.php

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
namespace Asseco\JsonQueryBuilder\Traits;
4+
5+
use Asseco\JsonQueryBuilder\SQLProviders\PgSQLFunctions;
6+
use Asseco\JsonQueryBuilder\SQLProviders\SQLFunctions;
7+
use Illuminate\Database\Eloquent\Builder;
8+
use Illuminate\Support\Facades\DB;
9+
10+
trait DatabaseFunctions
11+
{
12+
public Builder $builder;
13+
14+
protected array $providers = [
15+
"pgsql" => PgSQLFunctions::class,
16+
];
17+
18+
protected function areArgumentsValid(): void
19+
{
20+
parent::areArgumentsValid();
21+
foreach ($this->arguments as $argument) {
22+
SQLFunctions::validateArgument($argument);
23+
}
24+
}
25+
26+
private function applyAggregation(array $params): string
27+
{
28+
$column = array_pop($params);
29+
$provider = $this->builder->getModel()->connection ?? config("database.default");
30+
$functions = $this->providers[$provider] ?? SQLFunctions::class;
31+
32+
return array_reduce(array_reverse($params), function ($query, $param) use (
33+
$column,
34+
$functions
35+
) {
36+
$stat = $query ?? $column;
37+
return $functions::$param($stat);
38+
});
39+
}
40+
41+
protected function prepareArguments(): void
42+
{
43+
$this->arguments = array_map(function ($argument) {
44+
if (strpos($argument, ":") === false) {
45+
return $argument;
46+
}
47+
$split = explode(":", $argument);
48+
$apply = $this->applyAggregation($split);
49+
$alias = join("_", $split);
50+
51+
return DB::raw("{$apply} as {$alias}");
52+
}, $this->arguments);
53+
}
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare (strict_types = 1);
4+
namespace Asseco\JsonQueryBuilder\Tests\Unit\SQLProviders;
5+
6+
use Asseco\JsonQueryBuilder\SQLProviders\PgSQLFunctions;
7+
use Asseco\JsonQueryBuilder\SQLProviders\SQLFunctions;
8+
use \Asseco\JsonQueryBuilder\Tests\TestCase;
9+
10+
class PgSQLFunctionsTest extends TestCase
11+
{
12+
13+
public SQLFunctions $functions;
14+
15+
public function setUp(): void
16+
{
17+
parent::setUp();
18+
19+
$this->functions = new PgSQLFunctions();
20+
}
21+
22+
public function test_it_should_return_the_year_function()
23+
{
24+
$query = $this->functions::year('column');
25+
$this->assertEquals("EXTRACT(YEAR FROM column)", $query);
26+
}
27+
28+
public function test_it_should_return_the_month_function()
29+
{
30+
$query = $this->functions::month('column');
31+
$this->assertEquals("EXTRACT(MONTH FROM column)", $query);
32+
}
33+
34+
public function test_it_should_return_the_day_function()
35+
{
36+
$query = $this->functions::day('column');
37+
$this->assertEquals("EXTRACT(DAY FROM column)", $query);
38+
}
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
declare (strict_types = 1);
4+
namespace Asseco\JsonQueryBuilder\Tests\Unit\SQLProviders;
5+
6+
use Asseco\JsonQueryBuilder\SQLProviders\SQLFunctions;
7+
use \Asseco\JsonQueryBuilder\Tests\TestCase;
8+
9+
class SQLFunctionsTest extends TestCase
10+
{
11+
12+
public SQLFunctions $functions;
13+
14+
public function setUp(): void
15+
{
16+
parent::setUp();
17+
18+
$this->functions = new SQLFunctions();
19+
}
20+
public function test_it_returns_the_right_functions_query()
21+
{
22+
foreach (SQLFunctions::DB_FUNCTIONS as $fn) {
23+
$query = $this->functions::{$fn}('column');
24+
$this->assertEquals("{$fn}(column)", $query);
25+
}
26+
}
27+
28+
public function test_it_throws_an_exception_if_an_invalid_function_is_given()
29+
{
30+
$this->expectException(\Asseco\JsonQueryBuilder\Exceptions\JsonQueryBuilderException::class);
31+
$this->functions::invalidFunction('id');
32+
}
33+
34+
public function test_it_validate_sql_functions()
35+
{
36+
foreach (SQLFunctions::DB_FUNCTIONS as $fn) {
37+
$this->assertNull($this->functions::validateArgument($fn . ':column'));
38+
}
39+
}
40+
41+
public function test_it_validate_nested_functions_validation()
42+
{
43+
$this->assertNull($this->functions::validateArgument('avg:year:column'));
44+
}
45+
46+
public function test_it_bypass_when_no_function_is_given()
47+
{
48+
$this->assertNull($this->functions::validateArgument('column'));
49+
}
50+
51+
public function test_it_throws_an_exception_if_an_invalid_argument_is_given()
52+
{
53+
$this->expectException(\Asseco\JsonQueryBuilder\Exceptions\JsonQueryBuilderException::class);
54+
$this->functions::validateArgument('invalid:column');
55+
}
56+
57+
public function test_it_throws_an_exception_if_an_invalid_column_is_given()
58+
{
59+
$this->expectException(\Asseco\JsonQueryBuilder\Exceptions\JsonQueryBuilderException::class);
60+
$this->functions::validateArgument("avg:'this--sql-scripting-is-invalid'");
61+
}
62+
63+
public function test_it_throws_an_exception_if_no_column_is_given()
64+
{
65+
$this->expectException(\Asseco\JsonQueryBuilder\Exceptions\JsonQueryBuilderException::class);
66+
$this->functions::validateArgument('sum');
67+
}
68+
public function test_it_throws_an_exception_if_no_column_when_nested_is_given()
69+
{
70+
$this->expectException(\Asseco\JsonQueryBuilder\Exceptions\JsonQueryBuilderException::class);
71+
$this->functions::validateArgument('sum:avg');
72+
}
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
namespace Asseco\JsonQueryBuilder\Tests\Unit\Traits;
4+
5+
use Asseco\JsonQueryBuilder\Config\ModelConfig;
6+
use Asseco\JsonQueryBuilder\RequestParameters\AbstractParameter;
7+
use Asseco\JsonQueryBuilder\SQLProviders\SQLFunctions;
8+
use Asseco\JsonQueryBuilder\Tests\TestCase;
9+
use Asseco\JsonQueryBuilder\Traits\DatabaseFunctions;
10+
use Illuminate\Database\Eloquent\Builder;
11+
12+
class TestParameterClass extends AbstractParameter
13+
{
14+
use DatabaseFunctions;
15+
16+
public static function getParameterName(): string
17+
{
18+
return 'test';
19+
}
20+
protected function appendQuery(): void
21+
{
22+
$this->prepareArguments();
23+
$this->builder->addSelect($this->arguments);
24+
}
25+
}
26+
27+
class DatabaseFunctionsTest extends TestCase
28+
{
29+
protected Builder $builder;
30+
protected ModelConfig $modelConfig;
31+
32+
public function setUp(): void
33+
{
34+
parent::setUp();
35+
$this->builder = app(Builder::class);
36+
$this->modelConfig = \Mockery::mock(ModelConfig::class);
37+
38+
}
39+
40+
public function test_it_not_throw_with_regular_parameters()
41+
{
42+
$parameter = new TestParameterClass(["column_a", "column_b", "column_c", "column_d"], $this->builder, $this->modelConfig);
43+
$this->assertNull($parameter->run());
44+
$this->assertEquals('select "column_a", "column_b", "column_c", "column_d"', $parameter->builder->toSql());
45+
}
46+
47+
public function test_it_apply_aggregation_functions()
48+
{
49+
foreach (SQLFunctions::DB_FUNCTIONS as $fn) {
50+
$builder = $this->builder->clone();
51+
$parameter = new TestParameterClass(["$fn:column"], $builder, $this->modelConfig);
52+
$this->assertNull($parameter->run());
53+
$this->assertEquals("select $fn(column) as {$fn}_column", $builder->toSql());
54+
}
55+
56+
}
57+
58+
public function test_it_apply_nested_aggregation_functions()
59+
{
60+
$parameter = new TestParameterClass(["avg:day:column"], $this->builder, $this->modelConfig);
61+
$this->assertNull($parameter->run());
62+
$this->assertEquals("select avg(day(column)) as avg_day_column", $this->builder->toSql());
63+
}
64+
65+
public function test_it_uses_pgsql_syntax()
66+
{
67+
app("config")->set("database.default", "pgsql");
68+
$parameter = new TestParameterClass(["avg:day:column"], $this->builder, $this->modelConfig);
69+
$this->assertNull($parameter->run());
70+
$this->assertEquals("select avg(EXTRACT(DAY FROM column)) as avg_day_column", $this->builder->toSql());
71+
}
72+
}

0 commit comments

Comments
 (0)