Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/lib/postgresql/src/Flow/PostgreSql/DSL/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,23 @@ function sql_to_paginated_query(string $sql, int $limit, int $offset = 0) : stri
return $query->deparse();
}

/**
* Transform a SQL query to limit results to a specific number of rows.
*
* @param string $sql The SQL query to limit
* @param int $limit Maximum number of rows to return
*
* @return string The limited SQL query
*/
#[DocumentationDSL(module: Module::PG_QUERY, type: DSLType::HELPER)]
function sql_to_limited_query(string $sql, int $limit) : string
{
$query = (new Parser())->parse($sql);
$query->traverse(new PaginationModifier(new PaginationConfig($limit)));

return $query->deparse();
}

/**
* Transform a SQL query into a COUNT query for pagination.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,10 @@ public function summary() : PlanSummary
$hasDiskReads = false;
$totalSharedHit = 0;
$totalSharedRead = 0;
$hashJoinCount = 0;
$nestedLoopCount = 0;
$mergeJoinCount = 0;
$hasTempSpill = false;

foreach ($nodes as $node) {
if ($node->isSequentialScan()) {
Expand All @@ -431,12 +435,29 @@ public function summary() : PlanSummary
$hasExternalSort = true;
}

if ($node->isHashJoin()) {
$hashJoinCount++;
}

if ($node->isNestedLoop()) {
$nestedLoopCount++;
}

if ($node->isMergeJoin()) {
$mergeJoinCount++;
}

$buffers = $node->buffers();

if ($buffers !== null) {
if ($buffers->sharedRead() > 0) {
$hasDiskReads = true;
}

if ($buffers->hasDiskSpill()) {
$hasTempSpill = true;
}

$totalSharedHit += $buffers->sharedHit();
$totalSharedRead += $buffers->sharedRead();
}
Expand All @@ -455,6 +476,16 @@ public function summary() : PlanSummary
hasExternalSort: $hasExternalSort,
hasDiskReads: $hasDiskReads,
overallCacheHitRatio: $overallCacheHitRatio,
memoryUsed: $this->plan->memoryUsed(),
memoryPeak: $this->plan->memoryPeak(),
hashJoinCount: $hashJoinCount,
nestedLoopCount: $nestedLoopCount,
mergeJoinCount: $mergeJoinCount,
totalSharedHit: $totalSharedHit,
totalSharedRead: $totalSharedRead,
hasTempSpill: $hasTempSpill,
estimatedRows: $this->plan->rootNode()->estimatedRows(),
actualRows: $this->plan->rootNode()->actualRows(),
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,64 @@ public function __construct(
public bool $hasExternalSort,
public bool $hasDiskReads,
public ?float $overallCacheHitRatio,
public ?int $memoryUsed,
public ?int $memoryPeak,
public int $hashJoinCount,
public int $nestedLoopCount,
public int $mergeJoinCount,
public int $totalSharedHit,
public int $totalSharedRead,
public bool $hasTempSpill,
public int $estimatedRows,
public ?int $actualRows,
) {
}

/**
* @return array{
* total_cost: float,
* execution_time: ?float,
* planning_time: ?float,
* node_count: int,
* sequential_scan_count: int,
* index_scan_count: int,
* has_external_sort: bool,
* has_disk_reads: bool,
* overall_cache_hit_ratio: ?float,
* memory_used: ?int,
* memory_peak: ?int,
* hash_join_count: int,
* nested_loop_count: int,
* merge_join_count: int,
* total_shared_hit: int,
* total_shared_read: int,
* has_temp_spill: bool,
* estimated_rows: int,
* actual_rows: ?int
* }
*/
public function normalize() : array
{
return [
'total_cost' => $this->totalCost,
'execution_time' => $this->executionTime,
'planning_time' => $this->planningTime,
'node_count' => $this->nodeCount,
'sequential_scan_count' => $this->sequentialScanCount,
'index_scan_count' => $this->indexScanCount,
'has_external_sort' => $this->hasExternalSort,
'has_disk_reads' => $this->hasDiskReads,
'overall_cache_hit_ratio' => $this->overallCacheHitRatio,
'memory_used' => $this->memoryUsed,
'memory_peak' => $this->memoryPeak,
'hash_join_count' => $this->hashJoinCount,
'nested_loop_count' => $this->nestedLoopCount,
'merge_join_count' => $this->mergeJoinCount,
'total_shared_hit' => $this->totalSharedHit,
'total_shared_read' => $this->totalSharedRead,
'has_temp_spill' => $this->hasTempSpill,
'estimated_rows' => $this->estimatedRows,
'actual_rows' => $this->actualRows,
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

declare(strict_types=1);

namespace Flow\PostgreSql\Tests\Unit\AST\Transformers;

use function Flow\PostgreSql\DSL\sql_to_limited_query;

use PHPUnit\Framework\TestCase;

final class LimitedQueryTest extends TestCase
{
protected function setUp() : void
{
if (!\extension_loaded('pg_query')) {
self::markTestSkipped('pg_query extension is not loaded. For local development use `nix-shell --arg with-pg-query-ext true` to enable it.');
}
}

public function test_limit_does_not_add_offset() : void
{
$result = sql_to_limited_query('SELECT id, name FROM products ORDER BY name', 25);

self::assertSame('SELECT id, name FROM products ORDER BY name LIMIT 25', $result);
self::assertStringNotContainsString('OFFSET', $result);
}

public function test_limit_overrides_existing_limit() : void
{
$result = sql_to_limited_query('SELECT * FROM users LIMIT 100', 10);

self::assertSame('SELECT * FROM users LIMIT 10', $result);
}

public function test_limit_removes_existing_offset() : void
{
$result = sql_to_limited_query('SELECT * FROM users ORDER BY id LIMIT 100 OFFSET 50', 10);

self::assertSame('SELECT * FROM users ORDER BY id LIMIT 10', $result);
self::assertStringNotContainsString('OFFSET', $result);
}

public function test_limit_union_wraps_in_subquery() : void
{
$sql = 'SELECT id FROM users UNION SELECT id FROM admins ORDER BY id';

$result = sql_to_limited_query($sql, 10);

self::assertSame('SELECT * FROM (SELECT id FROM users UNION SELECT id FROM admins ORDER BY id) _pagination_subq LIMIT 10', $result);
self::assertStringNotContainsString('OFFSET', $result);
}

public function test_limit_with_joins() : void
{
$sql = 'SELECT u.id, o.total FROM users u JOIN orders o ON u.id = o.user_id';

$result = sql_to_limited_query($sql, 50);

self::assertSame('SELECT u.id, o.total FROM users u JOIN orders o ON u.id = o.user_id LIMIT 50', $result);
}

public function test_limit_with_where_clause() : void
{
$result = sql_to_limited_query('SELECT * FROM users WHERE active = true', 5);

self::assertSame('SELECT * FROM users WHERE active = true LIMIT 5', $result);
}

public function test_simple_select_with_limit() : void
{
$result = sql_to_limited_query('SELECT * FROM users', 10);

self::assertSame('SELECT * FROM users LIMIT 10', $result);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<?php

declare(strict_types=1);

namespace Flow\PostgreSql\Tests\Unit\Explain\Analyzer;

use Flow\PostgreSql\Explain\Analyzer\PlanSummary;
use PHPUnit\Framework\TestCase;

final class PlanSummaryTest extends TestCase
{
public function test_normalize_returns_all_fields() : void
{
$summary = new PlanSummary(
totalCost: 150.5,
executionTime: 25.0,
planningTime: 0.5,
nodeCount: 5,
sequentialScanCount: 2,
indexScanCount: 3,
hasExternalSort: true,
hasDiskReads: true,
overallCacheHitRatio: 0.95,
memoryUsed: 1024,
memoryPeak: 2048,
hashJoinCount: 1,
nestedLoopCount: 2,
mergeJoinCount: 0,
totalSharedHit: 100,
totalSharedRead: 5,
hasTempSpill: false,
estimatedRows: 1000,
actualRows: 950,
);

$normalized = $summary->normalize();

self::assertSame(150.5, $normalized['total_cost']);
self::assertSame(25.0, $normalized['execution_time']);
self::assertSame(0.5, $normalized['planning_time']);
self::assertSame(5, $normalized['node_count']);
self::assertSame(2, $normalized['sequential_scan_count']);
self::assertSame(3, $normalized['index_scan_count']);
self::assertTrue($normalized['has_external_sort']);
self::assertTrue($normalized['has_disk_reads']);
self::assertSame(0.95, $normalized['overall_cache_hit_ratio']);
self::assertSame(1024, $normalized['memory_used']);
self::assertSame(2048, $normalized['memory_peak']);
self::assertSame(1, $normalized['hash_join_count']);
self::assertSame(2, $normalized['nested_loop_count']);
self::assertSame(0, $normalized['merge_join_count']);
self::assertSame(100, $normalized['total_shared_hit']);
self::assertSame(5, $normalized['total_shared_read']);
self::assertFalse($normalized['has_temp_spill']);
self::assertSame(1000, $normalized['estimated_rows']);
self::assertSame(950, $normalized['actual_rows']);
}

public function test_normalize_returns_expected_keys() : void
{
$summary = new PlanSummary(
totalCost: 50.0,
executionTime: 10.0,
planningTime: 0.1,
nodeCount: 2,
sequentialScanCount: 1,
indexScanCount: 1,
hasExternalSort: false,
hasDiskReads: false,
overallCacheHitRatio: 1.0,
memoryUsed: 512,
memoryPeak: 1024,
hashJoinCount: 0,
nestedLoopCount: 0,
mergeJoinCount: 0,
totalSharedHit: 50,
totalSharedRead: 0,
hasTempSpill: false,
estimatedRows: 100,
actualRows: 100,
);

$normalized = $summary->normalize();

$expectedKeys = [
'total_cost',
'execution_time',
'planning_time',
'node_count',
'sequential_scan_count',
'index_scan_count',
'has_external_sort',
'has_disk_reads',
'overall_cache_hit_ratio',
'memory_used',
'memory_peak',
'hash_join_count',
'nested_loop_count',
'merge_join_count',
'total_shared_hit',
'total_shared_read',
'has_temp_spill',
'estimated_rows',
'actual_rows',
];

self::assertSame($expectedKeys, \array_keys($normalized));
}

public function test_normalize_with_null_values() : void
{
$summary = new PlanSummary(
totalCost: 100.0,
executionTime: null,
planningTime: null,
nodeCount: 1,
sequentialScanCount: 0,
indexScanCount: 0,
hasExternalSort: false,
hasDiskReads: false,
overallCacheHitRatio: null,
memoryUsed: null,
memoryPeak: null,
hashJoinCount: 0,
nestedLoopCount: 0,
mergeJoinCount: 0,
totalSharedHit: 0,
totalSharedRead: 0,
hasTempSpill: false,
estimatedRows: 50,
actualRows: null,
);

$normalized = $summary->normalize();

self::assertSame(100.0, $normalized['total_cost']);
self::assertNull($normalized['execution_time']);
self::assertNull($normalized['planning_time']);
self::assertNull($normalized['overall_cache_hit_ratio']);
self::assertNull($normalized['memory_used']);
self::assertNull($normalized['memory_peak']);
self::assertSame(50, $normalized['estimated_rows']);
self::assertNull($normalized['actual_rows']);
}
}
2 changes: 1 addition & 1 deletion web/landing/resources/dsl.json

Large diffs are not rendered by default.

Loading