SingleRowTable.php

PHP

Path: src/Table/Utility/SingleRowTable.php

<?php

namespace mini\Table\Utility;

use mini\Table\ColumnDef;
use mini\Table\Contracts\SetInterface;
use mini\Table\Contracts\TableInterface;
use mini\Table\Predicate;
use mini\Table\Types\ColumnType;
use mini\Table\Utility\EmptyTable;
use mini\Table\Utility\TablePropertiesTrait;
use mini\Table\Wrappers\AliasTable;
use mini\Table\Wrappers\UnionTable;
use Traversable;

/**
 * A table with exactly one row and dynamic columns
 *
 * Used as the base for SELECT without FROM (like Oracle's DUAL or SQL Server's constants).
 *
 * ```php
 * // SELECT 1, 2, 'hello'
 * $table = new SingleRowTable(['1' => 1, '2' => 2, "'hello'" => 'hello']);
 * ```
 */
final class SingleRowTable implements TableInterface
{
    use TablePropertiesTrait;

    /** @var array<string, mixed> column name => value */
    private array $values;

    /** @var array<string, ColumnDef> */
    private array $columns;

    /**
     * @param array<string, mixed> $values Column name => value pairs
     */
    public function __construct(array $values = [])
    {
        $this->values = $values;
        $this->columns = [];
        foreach ($values as $name => $value) {
            $type = match (true) {
                is_int($value) => ColumnType::Int,
                is_float($value) => ColumnType::Float,
                default => ColumnType::Text,
            };
            $this->columns[$name] = new ColumnDef($name, $type, indexed: true);
        }
    }

    public function getIterator(): Traversable
    {
        yield 1 => (object) $this->values;
    }

    public function count(): int
    {
        return 1;
    }

    public function getColumns(): array
    {
        return $this->columns;
    }

    public function getAllColumns(): array
    {
        return $this->columns;
    }

    public function has(object $member): bool
    {
        foreach ($this->values as $col => $val) {
            if (!property_exists($member, $col) || $member->$col !== $val) {
                return false;
            }
        }
        return true;
    }

    public function eq(string $column, int|float|string|null $value): TableInterface
    {
        if (!isset($this->values[$column])) {
            return EmptyTable::from($this);
        }
        return $this->values[$column] === $value ? $this : EmptyTable::from($this);
    }

    public function lt(string $column, int|float|string $value): TableInterface
    {
        if (!isset($this->values[$column])) {
            return EmptyTable::from($this);
        }
        return $this->values[$column] < $value ? $this : EmptyTable::from($this);
    }

    public function lte(string $column, int|float|string $value): TableInterface
    {
        if (!isset($this->values[$column])) {
            return EmptyTable::from($this);
        }
        return $this->values[$column] <= $value ? $this : EmptyTable::from($this);
    }

    public function gt(string $column, int|float|string $value): TableInterface
    {
        if (!isset($this->values[$column])) {
            return EmptyTable::from($this);
        }
        return $this->values[$column] > $value ? $this : EmptyTable::from($this);
    }

    public function gte(string $column, int|float|string $value): TableInterface
    {
        if (!isset($this->values[$column])) {
            return EmptyTable::from($this);
        }
        return $this->values[$column] >= $value ? $this : EmptyTable::from($this);
    }

    public function in(string $column, SetInterface $values): TableInterface
    {
        if (!isset($this->values[$column])) {
            return EmptyTable::from($this);
        }
        $member = (object) [$column => $this->values[$column]];
        return $values->has($member) ? $this : EmptyTable::from($this);
    }

    public function like(string $column, string $pattern): TableInterface
    {
        if (!isset($this->values[$column])) {
            return EmptyTable::from($this);
        }
        $regex = '/^' . str_replace(['%', '_'], ['.*', '.'], preg_quote($pattern, '/')) . '$/i';
        return preg_match($regex, (string)$this->values[$column]) ? $this : EmptyTable::from($this);
    }

    public function union(TableInterface $other): TableInterface
    {
        return new UnionTable($this, $other);
    }

    public function or(Predicate $a, Predicate $b, Predicate ...$more): TableInterface
    {
        // Single row - just check if any predicate matches
        foreach ([$a, $b, ...$more] as $p) {
            if ($p->test((object) $this->values)) {
                return $this;
            }
        }
        return EmptyTable::from($this);
    }

    public function except(SetInterface $other): TableInterface
    {
        return $other->has((object) $this->values) ? EmptyTable::from($this) : $this;
    }

    public function distinct(): TableInterface
    {
        return $this; // Single row is already distinct
    }

    public function columns(string ...$columns): TableInterface
    {
        $projected = [];
        foreach ($columns as $col) {
            if (isset($this->values[$col])) {
                $projected[$col] = $this->values[$col];
            }
        }
        return new self($projected);
    }

    public function order(?string $spec): TableInterface
    {
        return $this; // Single row needs no ordering
    }

    public function limit(?int $n): TableInterface
    {
        return $n === 0 ? EmptyTable::from($this) : $this;
    }

    public function offset(int $n): TableInterface
    {
        return $n > 0 ? EmptyTable::from($this) : $this;
    }

    public function getLimit(): ?int
    {
        return null;
    }

    public function getOffset(): int
    {
        return 0;
    }

    public function exists(): bool
    {
        return true;
    }

    public function load(string|int $rowId): ?object
    {
        return $rowId === 1 ? (object) $this->values : null;
    }

    public function withAlias(?string $tableAlias = null, array $columnAliases = []): TableInterface
    {
        return new AliasTable($this, $tableAlias, $columnAliases);
    }
}