Session.php

PHP

Path: src/Database/Session.php

<?php

namespace mini\Database;

use Closure;
use mini\Table\Contracts\TableInterface;

/**
 * Database session with isolated temporary tables
 *
 * A Session wraps a VirtualDatabase engine and provides:
 * - Isolated temporary table storage (CREATE TEMPORARY TABLE)
 * - Session-scoped state (future: prepared statement cache, transaction state)
 *
 * In fiber/coroutine environments, each fiber should have its own Session
 * to prevent temp table collisions. The mini\vdb() helper automatically
 * manages session-per-fiber via Lifetime::Scoped.
 *
 * ```php
 * // Get session (automatically scoped per fiber)
 * $db = mini\vdb();
 * $db->exec("CREATE TEMPORARY TABLE tmp (id INTEGER, val TEXT)");
 * $db->exec("INSERT INTO tmp VALUES (1, 'test')");
 * $result = $db->query("SELECT * FROM tmp");
 * // Temp table is automatically cleaned up when fiber ends
 * ```
 */
class Session implements DatabaseInterface
{
    /** @var array<string, TableInterface> Session-local temporary tables */
    private array $tempTables = [];

    /** @var string|null Last insert ID */
    private ?string $lastInsertId = null;

    public function __construct(
        private readonly VirtualDatabase $engine,
    ) {}

    /**
     * Get the underlying VirtualDatabase engine
     */
    public function getEngine(): VirtualDatabase
    {
        return $this->engine;
    }

    /**
     * Get a temporary table by name
     *
     * @internal Used by VirtualDatabase for table resolution
     */
    public function getTempTable(string $name): ?TableInterface
    {
        return $this->tempTables[$name] ?? null;
    }

    /**
     * Register a temporary table
     *
     * @internal Used by VirtualDatabase when executing CREATE TEMPORARY TABLE
     */
    public function setTempTable(string $name, TableInterface $table): void
    {
        $this->tempTables[$name] = $table;
    }

    /**
     * Drop a temporary table
     *
     * @internal Used by VirtualDatabase when executing DROP TABLE on temp tables
     */
    public function dropTempTable(string $name): bool
    {
        if (isset($this->tempTables[$name])) {
            unset($this->tempTables[$name]);
            return true;
        }
        return false;
    }

    /**
     * Check if a temporary table exists
     */
    public function hasTempTable(string $name): bool
    {
        return isset($this->tempTables[$name]);
    }

    /**
     * Get all temporary table names
     */
    public function getTempTableNames(): array
    {
        return array_keys($this->tempTables);
    }

    // =========================================================================
    // DatabaseInterface implementation - delegates to engine with session context
    // =========================================================================

    public function query(string $sql, array $params = []): Query
    {
        return $this->engine->queryWithSession($sql, $params, $this);
    }

    public function queryOne(string $sql, array $params = []): ?object
    {
        foreach ($this->query($sql, $params) as $row) {
            return $row;
        }
        return null;
    }

    public function queryField(string $sql, array $params = []): mixed
    {
        $row = $this->queryOne($sql, $params);
        if ($row === null) {
            return null;
        }
        $values = get_object_vars($row);
        return reset($values);
    }

    public function queryColumn(string $sql, array $params = []): array
    {
        $result = [];
        foreach ($this->query($sql, $params) as $row) {
            $values = get_object_vars($row);
            $result[] = reset($values);
        }
        return $result;
    }

    public function exec(string $sql, array $params = []): int
    {
        $result = $this->engine->execWithSession($sql, $params, $this);
        $this->lastInsertId = $this->engine->lastInsertId();
        return $result;
    }

    public function lastInsertId(): ?string
    {
        return $this->lastInsertId;
    }

    public function tableExists(string $tableName): bool
    {
        // Check temp tables first, then engine
        if (isset($this->tempTables[$tableName])) {
            return true;
        }
        return $this->engine->tableExists($tableName);
    }

    public function transaction(Closure $task): mixed
    {
        // VDB doesn't support real transactions, but we implement the interface
        return $this->engine->transaction($task);
    }

    public function getDialect(): SqlDialect
    {
        return $this->engine->getDialect();
    }

    public function quote(mixed $value): string
    {
        return $this->engine->quote($value);
    }

    public function quoteIdentifier(string $identifier): string
    {
        return $this->engine->quoteIdentifier($identifier);
    }

    public function delete(Query|PartialQuery $query): int
    {
        return $this->engine->delete($query);
    }

    public function update(Query|PartialQuery $query, string|array $set, array $params = []): int
    {
        return $this->engine->update($query, $set, $params);
    }

    public function insert(string $table, array $data): string
    {
        // Check if inserting into a temp table
        if (isset($this->tempTables[$table])) {
            $tempTable = $this->tempTables[$table];
            if ($tempTable instanceof \mini\Table\Contracts\MutableTableInterface) {
                $id = $tempTable->insert($data);
                $this->lastInsertId = (string) $id;
                return $this->lastInsertId;
            }
            throw new \RuntimeException("Temporary table '$table' is not mutable");
        }

        $result = $this->engine->insert($table, $data);
        $this->lastInsertId = $result;
        return $result;
    }

    public function upsert(string $table, array $data, string ...$conflictColumns): int
    {
        return $this->engine->upsert($table, $data, ...$conflictColumns);
    }

    public function withTables(array $tables): DatabaseInterface
    {
        return $this->engine->withTables($tables);
    }

    public function getSchema(): TableInterface
    {
        // TODO: Include temp tables in schema
        return $this->engine->getSchema();
    }
}