BarrierTable.php
PHP
Path: src/Table/Wrappers/BarrierTable.php
<?php
namespace mini\Table\Wrappers;
use mini\Table\AbstractTable;
use mini\Table\Contracts\SetInterface;
use mini\Table\Contracts\TableInterface;
use mini\Table\Types\Operator;
use mini\Table\OrderDef;
use mini\Table\Predicate;
use mini\Table\Utility\EmptyTable;
use mini\Table\Utility\TablePropertiesTrait;
/**
* Barrier that prevents filter/order pushdown to preserve result set membership
*
* When a table has LIMIT/OFFSET applied, further filtering should operate
* on those specific rows, not modify the underlying query. BarrierTable
* acts as a boundary that prevents filter pushdown.
*
* ```php
* // Without barrier: eq() modifies query, still returns 10 rows
* $users->order('age DESC')->limit(10)->eq('gender', 'male'); // 10 males
*
* // With barrier: eq() filters the 10 rows we already selected
* BarrierTable::from($users->order('age DESC')->limit(10))
* ->eq('gender', 'male'); // ~5 males from the original 10
* ```
*/
final class BarrierTable extends AbstractTableWrapper
{
/**
* Freeze a table if it has pagination, otherwise return as-is
*/
public static function from(AbstractTable $source): AbstractTable
{
if ($source->getLimit() === null && $source->getOffset() === 0) {
return $source;
}
return new self($source);
}
private function __construct(AbstractTable $source)
{
parent::__construct($source);
}
protected function materialize(string ...$additionalColumns): \Traversable
{
$offset = $this->offset;
$limit = $this->limit;
$count = 0;
$skipped = 0;
foreach ($this->source->materialize(...$additionalColumns) as $key => $row) {
if ($skipped < $offset) {
$skipped++;
continue;
}
if ($limit !== null && $count >= $limit) {
break;
}
yield $key => $row;
$count++;
}
}
public function count(): int
{
$sourceCount = $this->source->count();
$afterOffset = max(0, $sourceCount - $this->offset);
return $this->limit === null ? $afterOffset : min($afterOffset, $this->limit);
}
public function getLimit(): ?int
{
return null;
}
public function getOffset(): int
{
return 0;
}
// -------------------------------------------------------------------------
// Filter methods - return wrappers around $this, never push down
// -------------------------------------------------------------------------
public function eq(string $column, int|float|string|null $value): TableInterface
{
return new FilteredTable($this, $column, Operator::Eq, $value);
}
public function lt(string $column, int|float|string $value): TableInterface
{
return new FilteredTable($this, $column, Operator::Lt, $value);
}
public function lte(string $column, int|float|string $value): TableInterface
{
return new FilteredTable($this, $column, Operator::Lte, $value);
}
public function gt(string $column, int|float|string $value): TableInterface
{
return new FilteredTable($this, $column, Operator::Gt, $value);
}
public function gte(string $column, int|float|string $value): TableInterface
{
return new FilteredTable($this, $column, Operator::Gte, $value);
}
public function in(string $column, SetInterface $values): TableInterface
{
return new FilteredTable($this, $column, Operator::In, $values);
}
public function like(string $column, string $pattern): TableInterface
{
return new FilteredTable($this, $column, Operator::Like, $pattern);
}
public function or(Predicate $a, Predicate $b, Predicate ...$more): TableInterface
{
// Filter out empty predicates (match nothing)
$predicates = array_values(array_filter(
[$a, $b, ...$more],
fn($p) => !$p->isEmpty()
));
// No predicates → nothing matches
if (empty($predicates)) {
return EmptyTable::from($this);
}
// Don't push down - use OrTable for single-pass evaluation
return new OrTable($this, ...$predicates);
}
// -------------------------------------------------------------------------
// Order/limit/offset - return wrappers, don't modify source
// -------------------------------------------------------------------------
public function order(?string $spec): TableInterface
{
$orders = $spec ? OrderDef::parse($spec) : [];
if (empty($orders)) {
return $this;
}
return new SortedTable($this, ...$orders);
}
public function limit(?int $n): TableInterface
{
// Apply limit on top of frozen rows
$c = clone $this;
$c->limit = $n;
return $c;
}
public function offset(int $n): TableInterface
{
// Apply offset on top of frozen rows
$c = clone $this;
$c->offset = $n;
return $c;
}
}