Skip to content

Commit

Permalink
Tablesample walker
Browse files Browse the repository at this point in the history
  • Loading branch information
tugmaks committed Nov 24, 2023
1 parent ae21b09 commit 1aad9ac
Show file tree
Hide file tree
Showing 10 changed files with 248 additions and 12 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

1.2
---

* TABLESAMPLE walker (SYSTEM/BERNOULLI)

1.1
---

Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,19 @@ $query->getSQL();
Output:
```sql
SELECT u0_.id AS id_0, u0_.name AS name_1 FROM users u0_ ORDER BY u0_.name DESC NULLS LAST
```

## Tablesample walker
### Example
```php
$query = $this->entityManager->createQuery('SELECT u FROM App\Entity\User u ORDER BY u.name DESC');

$query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, TablesampleWalker::class);
$query->setHint(TablesampleWalker::TABLESAMPLE_RULE, [User::class => new Tablesample(TablesampleMethod::BERNOULLI, 0.1) ]);

$query->getSQL();
```
Output:
```sql
SELECT u0_.id AS id_0, u0_.name AS name_1 FROM users u0_ TABLESAMPLE BERNOULLI(0.1) ORDER BY u0_.name DESC
```
1 change: 1 addition & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
includes:
- vendor/phpstan/phpstan-doctrine/extension.neon
- vendor/phpstan/phpstan/conf/bleedingEdge.neon
- vendor/phpstan/phpstan-phpunit/extension.neon
parameters:
tmpDir: var/phpstan
level: 9
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\SqlWalker;

final class NullsWalkers extends SqlWalker
final class NullsWalker extends SqlWalker
{
public const NULLS_RULE = 'NullsWalkers.Rule';
public const NULLS_RULE = 'NullsWalker.Rule';

/**
* {@inheritDoc}
Expand Down
33 changes: 33 additions & 0 deletions src/Tablesample/Tablesample.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

/**
* Copyright (c) 2023 Maksim Tugaev
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*
* @see https://github.com/tugmaks/doctrine-walkers
*/

namespace Tugmaks\DoctrineWalkers\Tablesample;

final class Tablesample
{
public function __construct(private readonly TablesampleMethod $tablesampleMethod, private readonly float $percentage)
{
if (0.00 > $percentage || 100.00 < $percentage) {
throw new TablesampleWalkerException('A percentage must be between 0 and 100');
}
}

public function toSQL(): string
{
return 'TABLESAMPLE ' .
$this->tablesampleMethod->name .
'(' .
$this->percentage .
')';
}
}
20 changes: 20 additions & 0 deletions src/Tablesample/TablesampleMethod.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

/**
* Copyright (c) 2023 Maksim Tugaev
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*
* @see https://github.com/tugmaks/doctrine-walkers
*/

namespace Tugmaks\DoctrineWalkers\Tablesample;

enum TablesampleMethod
{
case BERNOULLI;
case SYSTEM;
}
50 changes: 50 additions & 0 deletions src/Tablesample/TablesampleWalker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

/**
* Copyright (c) 2023 Maksim Tugaev
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*
* @see https://github.com/tugmaks/doctrine-walkers
*/

namespace Tugmaks\DoctrineWalkers\Tablesample;

use Doctrine\ORM\Query;
use Doctrine\ORM\Query\SqlWalker;

final class TablesampleWalker extends SqlWalker
{
public const TABLESAMPLE_RULE = 'TablesampleWalker.Rule';

/**
* @param \Doctrine\ORM\Query\AST\FromClause $fromClause
*
* @throws Query\QueryException
*/
public function walkFromClause($fromClause): string
{
/** @var array<\Doctrine\ORM\Query\AST\IdentificationVariableDeclaration> $identificationVarDecls */
$identificationVarDecls = $fromClause->identificationVariableDeclarations;
$sqlParts = [];

/** @var array<class-string, Tablesample> $hint */
$hint = $this->getQuery()->getHint(self::TABLESAMPLE_RULE);

foreach ($identificationVarDecls as $identificationVariableDecl) {
$sqlPart = $this->walkIdentificationVariableDeclaration($identificationVariableDecl);
$candidate = $hint[$identificationVariableDecl->rangeVariableDeclaration?->abstractSchemaName] ?? null;

if ($candidate instanceof Tablesample) {
$sqlPart .= ' ' . $candidate->toSQL();
}

$sqlParts[] = $sqlPart;
}

return ' FROM ' . \implode(', ', $sqlParts);
}
}
20 changes: 20 additions & 0 deletions src/Tablesample/TablesampleWalkerException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

/**
* Copyright (c) 2023 Maksim Tugaev
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*
* @see https://github.com/tugmaks/doctrine-walkers
*/

namespace Tugmaks\DoctrineWalkers\Tablesample;

use LogicException;

final class TablesampleWalkerException extends LogicException
{
}
20 changes: 10 additions & 10 deletions tests/Ordering/NullsWalkerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@

use Doctrine\ORM\Query;
use Tugmaks\DoctrineWalkers\Ordering\NULLS;
use Tugmaks\DoctrineWalkers\Ordering\NullsWalker;
use Tugmaks\DoctrineWalkers\Ordering\NullsWalkerException;
use Tugmaks\DoctrineWalkers\Ordering\NullsWalkers;
use Tugmaks\DoctrineWalkersTest\AbstractWalkerTestCase;
use Tugmaks\DoctrineWalkersTest\DummyEntity;

final class NullsWalkerTest extends AbstractWalkerTestCase
{
/**
* @covers \Tugmaks\DoctrineWalkers\Ordering\NullsWalkers
* @covers \Tugmaks\DoctrineWalkers\Ordering\NullsWalker
*
* @dataProvider orderings
*
Expand All @@ -33,8 +33,8 @@ public function testNullWalker(string $dql, array $hint, string $sql): void
{
$query = $this->entityManager->createQuery($dql);

$query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, NullsWalkers::class);
$query->setHint(NullsWalkers::NULLS_RULE, $hint);
$query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, NullsWalker::class);
$query->setHint(NullsWalker::NULLS_RULE, $hint);

self::assertSame($sql, $query->getSQL());
}
Expand Down Expand Up @@ -64,7 +64,7 @@ public static function orderings(): iterable
}

/**
* @covers \Tugmaks\DoctrineWalkers\Ordering\NullsWalkers
* @covers \Tugmaks\DoctrineWalkers\Ordering\NullsWalker
*/
public function testItThrowExceptionIfHintIsInvalid(): void
{
Expand All @@ -75,14 +75,14 @@ public function testItThrowExceptionIfHintIsInvalid(): void

$query = $this->entityManager->createQuery($dql);

$query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, NullsWalkers::class);
$query->setHint(NullsWalkers::NULLS_RULE, ['d.name' => 'NULLS enum case expected here, but string provided']);
$query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, NullsWalker::class);
$query->setHint(NullsWalker::NULLS_RULE, ['d.name' => 'NULLS enum case expected here, but string provided']);

$query->getSQL();
}

/**
* @covers \Tugmaks\DoctrineWalkers\Ordering\NullsWalkers
* @covers \Tugmaks\DoctrineWalkers\Ordering\NullsWalker
*/
public function testItThrowsAnErrorIfHintIsNotArray(): void
{
Expand All @@ -93,8 +93,8 @@ public function testItThrowsAnErrorIfHintIsNotArray(): void

$query = $this->entityManager->createQuery($dql);

$query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, NullsWalkers::class);
$query->setHint(NullsWalkers::NULLS_RULE, 'foo');
$query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, NullsWalker::class);
$query->setHint(NullsWalker::NULLS_RULE, 'foo');

$query->getSQL();
}
Expand Down
92 changes: 92 additions & 0 deletions tests/Tablesample/TablesampleWalkerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php

declare(strict_types=1);

/**
* Copyright (c) 2023 Maksim Tugaev
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*
* @see https://github.com/tugmaks/doctrine-walkers
*/

namespace Tugmaks\DoctrineWalkersTest\Tablesample;

use Doctrine\ORM\Query;
use PHPUnit\Framework\Attributes\DataProvider;
use Tugmaks\DoctrineWalkers\Ordering\NULLS;
use Tugmaks\DoctrineWalkers\Tablesample\Tablesample;
use Tugmaks\DoctrineWalkers\Tablesample\TablesampleMethod;
use Tugmaks\DoctrineWalkers\Tablesample\TablesampleWalker;
use Tugmaks\DoctrineWalkers\Tablesample\TablesampleWalkerException;
use Tugmaks\DoctrineWalkersTest\AbstractWalkerTestCase;
use Tugmaks\DoctrineWalkersTest\DummyEntity;

final class TablesampleWalkerTest extends AbstractWalkerTestCase
{
/**
* @covers \Tugmaks\DoctrineWalkers\Tablesample\TablesampleWalker
*
* @dataProvider tablesamples
*
* @param array<string, NULLS> $hint
*/
public function testTablesampleWalker(string $dql, array $hint, string $sql): void
{
$query = $this->entityManager->createQuery($dql);

$query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, TablesampleWalker::class);
$query->setHint(TablesampleWalker::TABLESAMPLE_RULE, $hint);

self::assertSame($sql, $query->getSQL());
}

/**
* @return iterable<string, array{0:string, 1: array<class-string, Tablesample>, 2:string}>
*/
public static function tablesamples(): iterable
{
yield 'BERNOULLI(0.1)' => [
\sprintf('SELECT d FROM %s d ORDER BY d.name DESC', DummyEntity::class),
[DummyEntity::class => new Tablesample(TablesampleMethod::BERNOULLI, 0.1)],
'SELECT d0_.id AS id_0, d0_.name AS name_1 FROM de_tbl d0_ TABLESAMPLE BERNOULLI(0.1) ORDER BY d0_.name DESC',
];

yield 'SYSTEM(0.2)' => [
\sprintf('SELECT d FROM %s d ORDER BY d.name DESC', DummyEntity::class),
[DummyEntity::class => new Tablesample(TablesampleMethod::SYSTEM, 0.2)],
'SELECT d0_.id AS id_0, d0_.name AS name_1 FROM de_tbl d0_ TABLESAMPLE SYSTEM(0.2) ORDER BY d0_.name DESC',
];
}

/**
* @covers \Tugmaks\DoctrineWalkers\Tablesample\TablesampleWalker
*
* @dataProvider invalidPercentage
*/
public function testItThrowExceptionOnInvalidPercentage(float $percentage): void
{
self::expectException(TablesampleWalkerException::class);
self::expectExceptionMessage('A percentage must be between 0 and 100');

$dql = \sprintf('SELECT d FROM %s d ORDER BY d.name DESC', DummyEntity::class);

$query = $this->entityManager->createQuery($dql);

$query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, Tablesample::class);
$query->setHint(TablesampleWalker::TABLESAMPLE_RULE, [DummyEntity::class => new Tablesample(TablesampleMethod::SYSTEM, $percentage)]);

$query->getSQL();
}

/**
* @return iterable<int, array<float>>
*/
public static function invalidPercentage(): iterable
{
yield [-1.00];

yield [101.00];
}
}

0 comments on commit 1aad9ac

Please sign in to comment.