mini\Database\Virtual
namespace
Virtual Database System
SQL interface to non-SQL data sources via TableInterface.
Quick Start
use mini\Database\VirtualDatabase;
use mini\Table\FilteredTable;
$vdb = new VirtualDatabase();
// Register a PartialQuery as a virtual table (SQL-backed view)
$vdb->registerTable('active_users', db()->from('users')->eq('active', 1));
// Register in-memory data using FilteredTable
$vdb->registerTable('countries', new FilteredTable(
source: new class implements TableInterface {
public function getIterator(): Traversable {
yield 'no' => (object)['code' => 'NO', 'name' => 'Norway', 'continent' => 'Europe'];
yield 'se' => (object)['code' => 'SE', 'name' => 'Sweden', 'continent' => 'Europe'];
yield 'us' => (object)['code' => 'US', 'name' => 'United States', 'continent' => 'North America'];
}
// ... implement other TableInterface methods
}
));
// Query with SQL
foreach ($vdb->query("SELECT * FROM countries WHERE continent = ?", ['Europe']) as $row) {
echo $row->name; // Note: rows are stdClass objects
}
Architecture
Core Interfaces (in mini\Table)
SetInterface- Membership testing for IN clauses (has())TableInterface- Read-only table with filtering (eq,lt,gt,in,like,union,except), ordering, and paginationMutableTableInterface- Extends TableInterface withinsert(),update(),delete()
Classes
VirtualDatabase- ImplementsDatabaseInterface, parses SQL, translates to TableInterface callsFilteredTable- Composable wrapper that delegates filters to source, handles ordering/limit/offsetUnionTable- Union of two tables with filter pushdown to both sidesSet- Simple in-memory set for IN clausesCollation- Helper for creating collators (binary, nocase, locale-specific)
TableInterface
All table implementations must be immutable - each method returns a new instance:
$all = $table;
$active = $table->eq('status', 'active'); // $all unchanged
$sorted = $active->order('name'); // $active unchanged
Iteration yields row ID as key and row data as stdClass:
foreach ($table as $rowId => $row) {
// $rowId: int|string unique identifier
// $row: stdClass with column properties
echo $row->name;
}
Methods
interface TableInterface extends SetInterface, IteratorAggregate, Countable
{
// Filtering
public function eq(string $column, int|float|string|null $value): TableInterface;
public function lt(string $column, int|float|string $value): TableInterface;
public function lte(string $column, int|float|string $value): TableInterface;
public function gt(string $column, int|float|string $value): TableInterface;
public function gte(string $column, int|float|string $value): TableInterface;
public function in(string $column, SetInterface $values): TableInterface;
public function like(string $column, string $pattern): TableInterface;
// Set operations (for OR and NOT)
public function union(TableInterface $other): TableInterface;
public function except(TableInterface $other): TableInterface;
// Projection (for subqueries)
public function columns(string ...$columns): TableInterface;
public function has(string|int|float|stdClass $member): bool;
// Ordering and pagination
public function order(string $spec): TableInterface;
public function limit(int $n): TableInterface;
public function offset(int $n): TableInterface;
}
FilteredTable
Composable wrapper that delegates filter methods to source:
use mini\Table\FilteredTable;
class CsvTable implements TableInterface
{
public function eq(string $column, $value): TableInterface
{
return new FilteredTable(
source: $this,
filter: fn($row) => ($row->$column ?? null) === $value,
// orderFn: optional - return ordered TableInterface if source has index
);
}
// Other filter methods delegate similarly...
}
Order Optimization
FilteredTable supports efficient ordering via orderFn:
new FilteredTable(
source: $this,
filter: fn($row) => $row->status === 'active',
orderFn: fn($spec) => $this->hasIndexFor($spec) ? $this->order($spec) : null,
);
- If
orderFnreturns aTableInterface, FilteredTable streams from it (pre-sorted) - If
orderFnreturnsnull, FilteredTable buffers and sorts in-memory
UnionTable
Handles OR operations by pushing filters to both sides:
// WHERE status = 'active' OR role = 'admin'
$result = $table->eq('status', 'active')->union($table->eq('role', 'admin'));
// Further filtering pushes to both sides:
$result->gt('age', 18); // Becomes: (active AND age>18) UNION (admin AND age>18)
Using PartialQuery as a Virtual Table
Since PartialQuery implements TableInterface, you can register SQL-backed views:
// Create a filtered view of a real table
$vdb->registerTable('friends',
db()->from('users')->eq('relationship', 'friend')
);
// Query it with additional filters
$vdb->query("SELECT * FROM friends WHERE age > ?", [25]);
// Translates to: SELECT * FROM users WHERE relationship = 'friend' AND age > 25
MutableTableInterface
For tables that support write operations:
interface MutableTableInterface extends TableInterface
{
public function insert(array $row): int|string;
public function update(array $changes): int;
public function delete(): int;
}
UPDATE and DELETE operate on the current filtered state:
$table->eq('status', 'inactive')->delete(); // DELETE WHERE status = 'inactive'
$table->gt('age', 65)->update(['retired' => true]); // UPDATE WHERE age > 65
SQL Translation
VirtualDatabase translates SQL WHERE clauses to TableInterface method calls:
| SQL | TableInterface |
|---|---|
column = ? |
eq('column', $value) |
column < ? |
lt('column', $value) |
column <= ? |
lte('column', $value) |
column > ? |
gt('column', $value) |
column >= ? |
gte('column', $value) |
column IS NULL |
eq('column', null) |
column IS NOT NULL |
except(eq('column', null)) |
column != ? |
except(eq('column', $value)) |
column IN (...) |
in('column', new Set([...])) |
column LIKE ? |
like('column', $pattern) |
a AND b |
Chain: ->eq(...)->gt(...) |
a OR b |
union() |
NOT a |
except(a) |
ORDER BY col DESC |
order('col DESC') |
LIMIT n |
limit(n) |
Helper Function
Access VirtualDatabase via the vdb() helper:
// Configure in _config/mini/Database/VirtualDatabase.php
$result = vdb()->query("SELECT * FROM countries WHERE continent = ?", ['Europe']);
$row = vdb()->queryOne("SELECT * FROM users WHERE id = ?", [123]);
$count = vdb()->queryField("SELECT COUNT(*) FROM products");