SqlDialect.php

PHP

Path: src/Database/SqlDialect.php

<?php

namespace mini\Database;

/**
 * SQL dialect enumeration for database-specific SQL generation
 *
 * Dialects represent different SQL syntax variations across database vendors.
 * Use the most specific dialect when possible, fallback to Generic for unknown databases.
 *
 * @see DatabaseInterface::getDialect()
 */
enum SqlDialect
{
    /**
     * MySQL / MariaDB dialect
     *
     * Key differences:
     * - LIMIT syntax: LIMIT offset, count (non-standard)
     * - Identifier quotes: backticks `table_name`
     * - Case sensitivity: varies by storage engine and OS
     * - Non-standard functions and extensions
     */
    case MySQL;

    /**
     * PostgreSQL dialect
     *
     * Key differences:
     * - LIMIT syntax: LIMIT count OFFSET offset (SQL standard)
     * - Identifier quotes: double quotes "table_name"
     * - Case sensitive by default
     * - Strong standards compliance with useful extensions
     */
    case Postgres;

    /**
     * SQLite dialect
     *
     * Key differences:
     * - LIMIT syntax: LIMIT count OFFSET offset (SQL standard)
     * - Identifier quotes: double quotes "table_name" (also accepts backticks)
     * - Very permissive type system (dynamic typing)
     * - Simplified feature set (no RIGHT JOIN, limited ALTER TABLE)
     */
    case Sqlite;

    /**
     * Microsoft SQL Server dialect
     *
     * Key differences:
     * - LIMIT syntax: OFFSET offset ROWS FETCH NEXT count ROWS ONLY (SQL:2008)
     *   (Pre-2012 versions require ROW_NUMBER() windowing - not supported here)
     * - Identifier quotes: square brackets [table_name]
     * - TOP keyword for simple limits
     * - Significantly different from other databases
     */
    case SqlServer;

    /**
     * Oracle Database dialect
     *
     * Key differences:
     * - LIMIT syntax: Modern (12c+) uses OFFSET/FETCH, older uses ROWNUM
     * - Identifier quotes: double quotes "table_name"
     * - VARCHAR2, NUMBER types instead of VARCHAR, NUMERIC
     * - Unique handling of NULL vs empty strings
     */
    case Oracle;

    /**
     * Generic ANSI SQL dialect (fallback for unknown databases)
     *
     * Uses SQL:2016 standard syntax where possible:
     * - LIMIT syntax: LIMIT count OFFSET offset
     * - Identifier quotes: double quotes "table_name"
     * - Standard string escaping
     *
     * Use this for:
     * - Unknown/unsupported database vendors
     * - Testing and prototyping
     * - Maximum portability (may not use vendor-specific optimizations)
     */
    case Generic;

    /**
     * Virtual database dialect (in-memory, CSV, API backends)
     *
     * Key differences:
     * - No CTE/WITH support
     * - No JOIN support
     * - LIMIT syntax: LIMIT count OFFSET offset
     * - Identifier quotes: double quotes "table_name"
     * - Subqueries supported via lazy evaluation
     *
     * Used by VirtualDatabase for non-SQL data sources.
     */
    case Virtual;

    /**
     * Get the recommended identifier quote character for this dialect
     */
    public function getIdentifierQuote(): string
    {
        return match($this) {
            self::MySQL => '`',
            self::SqlServer => '[',  // Opening bracket, closing is ']'
            self::Postgres, self::Sqlite, self::Oracle, self::Generic, self::Virtual => '"',
        };
    }

    /**
     * Quote an identifier according to dialect rules
     *
     * Handles escaping of the quote character within the identifier.
     */
    public function quoteIdentifier(string $identifier): string
    {
        return match($this) {
            self::MySQL => '`' . str_replace('`', '``', $identifier) . '`',
            self::SqlServer => '[' . str_replace(']', ']]', $identifier) . ']',
            default => '"' . str_replace('"', '""', $identifier) . '"',
        };
    }

    /**
     * Check if this dialect uses standard LIMIT/OFFSET syntax
     */
    public function usesStandardLimit(): bool
    {
        return match($this) {
            self::MySQL => false,  // Uses LIMIT offset, count
            self::SqlServer => false,  // Uses OFFSET/FETCH
            self::Postgres, self::Sqlite, self::Oracle, self::Generic, self::Virtual => true,
        };
    }

    /**
     * Check if this dialect supports subqueries (e.g., IN (SELECT ...))
     */
    public function supportsSubquery(): bool
    {
        return true;
    }

    /**
     * Check if this dialect supports EXCEPT/INTERSECT set operations
     */
    public function supportsExcept(): bool
    {
        return match($this) {
            self::MySQL => false,  // MySQL doesn't support EXCEPT/INTERSECT
            self::Postgres, self::Sqlite, self::SqlServer, self::Oracle, self::Generic, self::Virtual => true,
        };
    }

    /**
     * Get human-readable dialect name
     */
    public function getName(): string
    {
        return match($this) {
            self::MySQL => 'MySQL',
            self::Postgres => 'PostgreSQL',
            self::Sqlite => 'SQLite',
            self::SqlServer => 'SQL Server',
            self::Oracle => 'Oracle',
            self::Generic => 'Generic ANSI SQL',
            self::Virtual => 'Virtual Database',
        };
    }
}