ColumnDef.php
PHP
Path: src/Table/ColumnDef.php
<?php
namespace mini\Table;
use mini\Table\Types\ColumnType;
use mini\Table\Types\IndexType;
/**
* Column definition with type and optional index metadata
*
* The type determines comparison semantics (whether to use collation).
* When a column is the leading column of an index, set $index to the
* index type and list any additional columns in $indexWith.
*
* Examples:
* ```php
* // Simple text column, no index
* new ColumnDef('name')
*
* // Integer primary key
* new ColumnDef('id', ColumnType::Int, IndexType::Primary)
*
* // Indexed text column
* new ColumnDef('email', ColumnType::Text, IndexType::Unique)
*
* // Composite index on (org_id, user_id) - define on leading column
* new ColumnDef('org_id', ColumnType::Int, IndexType::Index, [], 'user_id')
*
* // DateTime column (sorts correctly with binary comparison)
* new ColumnDef('created_at', ColumnType::DateTime)
*
* // Decimal with fixed scale (2 decimal places)
* new ColumnDef('price', ColumnType::Decimal, typeParameters: ['scale' => 2])
* ```
*/
readonly class ColumnDef
{
/** @var string[] Additional columns in composite index */
public array $indexWith;
/**
* @param string $name Column name
* @param ColumnType $type Data type for comparison semantics
* @param IndexType $index Index type (None if not indexed)
* @param array $typeParameters Type-specific parameters (e.g., ['scale' => 2] for Decimal)
* @param string ...$indexWith Additional columns in composite index
*/
public function __construct(
public string $name,
public ColumnType $type = ColumnType::Text,
public IndexType $index = IndexType::None,
public array $typeParameters = [],
string ...$indexWith,
) {
$this->indexWith = $indexWith;
}
/**
* Get type parameter with optional default
*/
public function getTypeParam(string $key, mixed $default = null): mixed
{
return $this->typeParameters[$key] ?? $default;
}
/**
* Get scale for Decimal columns (default: 0)
*/
public function getScale(): int
{
return (int) ($this->typeParameters['scale'] ?? 0);
}
/**
* Get all columns in this index (including this column)
*
* @return string[] Column names in index order, or empty if not indexed
*/
public function getIndexColumns(): array
{
if (!$this->index->isIndexed()) {
return [];
}
return [$this->name, ...$this->indexWith];
}
/**
* Check if this index can efficiently handle the given order columns
*
* The index can handle ordering if the order columns are a prefix of
* the index columns.
*
* @param string[] $orderColumns Column names in order
*/
public function canOrder(array $orderColumns): bool
{
if (!$this->index->isIndexed()) {
return false;
}
$indexCols = $this->getIndexColumns();
// Order columns must be a prefix of index columns
if (count($orderColumns) > count($indexCols)) {
return false;
}
return array_slice($indexCols, 0, count($orderColumns)) === $orderColumns;
}
/**
* Get the common denominator ColumnDef
*
* Returns a ColumnDef with the same type, weaker index type, and common
* prefix of indexWith columns. Type parameters must match exactly.
*
* ```php
* $a = new ColumnDef('id', ColumnType::Int, IndexType::Primary);
* $b = new ColumnDef('id', ColumnType::Int, IndexType::Index);
* $a->commonWith($b); // ColumnDef('id', ColumnType::Int, IndexType::Index)
*
* $a = new ColumnDef('a', ColumnType::Int, IndexType::Index, [], 'b', 'c');
* $b = new ColumnDef('a', ColumnType::Int, IndexType::Index, [], 'b', 'd');
* $a->commonWith($b); // ColumnDef('a', ColumnType::Int, IndexType::Index, [], 'b')
* ```
*
* @throws \InvalidArgumentException if column names, types, or type parameters don't match
*/
public function commonWith(self $other): self
{
if ($this->name !== $other->name) {
throw new \InvalidArgumentException(
"Cannot union columns with different names: {$this->name} vs {$other->name}"
);
}
if ($this->type !== $other->type) {
throw new \InvalidArgumentException(
"Cannot union columns with different types: {$this->name} has {$this->type->name} vs {$other->type->name}"
);
}
// Type parameters must match exactly (e.g., scale for Decimal)
if ($this->typeParameters !== $other->typeParameters) {
throw new \InvalidArgumentException(
"Cannot union columns with different type parameters: {$this->name}"
);
}
// Take the weaker index type
$index = $this->index->weakerOf($other->index);
if (!$index->isIndexed()) {
return new self($this->name, $this->type, IndexType::None, $this->typeParameters);
}
// Find common prefix of indexWith
$commonIndexWith = [];
$minLen = min(count($this->indexWith), count($other->indexWith));
for ($i = 0; $i < $minLen; $i++) {
if ($this->indexWith[$i] === $other->indexWith[$i]) {
$commonIndexWith[] = $this->indexWith[$i];
} else {
break;
}
}
return new self($this->name, $this->type, $index, $this->typeParameters, ...$commonIndexWith);
}
}